From f4ce1afce07c2049005403d64af635b8c54200a2 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Fri, 15 Nov 2024 21:55:24 +0900 Subject: [PATCH 01/16] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20API,=20=EA=B2=BD=EB=A7=A4=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 정식 경매 전환 반환값 변경 * feat: 이미 정식 경매인 경매를 전환할때 에러 코드 추가 * refactor: 경매의 소유자 유효성 검사 도메인 함수로 분리 - 경매 전환 메서드 함수 추가 - 경매의 첫번째 이미지 경로를 가져오는 함수 추가 * feat: 경매의 이미지가 없을 시 에러 코드 추가 * feat: 경매의 이미지가 없을 시 큰 오류이기에 에러 로깅 추가 * feat: 경매 전환 시작 서비스 함수 추가 * feat: 카테고리 응답 DTO 추가 * feat: 경매의 카테고리 종류 찾아오는 api 추가 * feat: String To Enum 변환 유틸 클래스 추가 * feat: 경매 카테고리 조회 서비스 함수 추가 * refactor: StringToEnum 유틸 클래스 적용 * chore: 카테고리 api security 허용 * refactor: 경매 소유 유효성 검사 함수 적용 * test: 경매 소유 유효성 검사 함수 변경으로 인한 테스트 코드 수정 * style: 로깅 변경 * refactor: 경매 소유 유효성 검사 리팩토링 * test: 경매 도메인 함수 테스트 추가 * test: 경매 전환 서비스 함수 테스트 추가 * style: 불필요한 코드 제거 * docs: 카테고리 응답 dto 문서 추가 * test: String To Enum 유티클래스 테스트 추가 --- .../market/common/config/SecurityConfig.java | 2 + .../common/util/StringCaseConverter.java | 32 +++++++ .../util/StringToEnumConverterFactory.java | 2 +- .../controller/AuctionDetailApi.java | 5 +- .../controller/AuctionDetailController.java | 10 +- .../auctionv2/controller/AuctionV2Api.java | 4 +- .../controller/AuctionV2Controller.java | 13 ++- .../dto/response/CategoryResponse.java | 8 ++ .../domain/auctionv2/entity/AuctionV2.java | 38 +++++++- .../auctionv2/error/AuctionErrorCode.java | 3 + .../service/AuctionCategoryService.java | 26 ++++++ .../service/AuctionDeleteService.java | 4 +- .../service/AuctionStartService.java | 52 +++++++++++ .../domain/imagev2/error/ImageErrorCode.java | 4 +- .../common/util/StringCaseConverterTest.java | 34 +++++++ .../auctionv2/entity/AuctionV2Test.java | 91 +++++++++++++++++++ .../service/AuctionDeleteServiceTest.java | 10 +- .../service/AuctionStartServiceTest.java | 72 +++++++++++++++ 18 files changed, 384 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/chzz/market/common/util/StringCaseConverter.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java create mode 100644 src/test/java/org/chzz/market/common/util/StringCaseConverterTest.java create mode 100644 src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java create mode 100644 src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java diff --git a/src/main/java/org/chzz/market/common/config/SecurityConfig.java b/src/main/java/org/chzz/market/common/config/SecurityConfig.java index 7bec3102..89422033 100644 --- a/src/main/java/org/chzz/market/common/config/SecurityConfig.java +++ b/src/main/java/org/chzz/market/common/config/SecurityConfig.java @@ -71,6 +71,8 @@ public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception "/api/v1/notifications/subscribe", "/api/v1/users/*", "/api/v1/users/check/nickname/*").permitAll() + .requestMatchers(GET, + "/api/v2/auctions/categories").permitAll() .requestMatchers(POST, "/api/v1/users/tokens/reissue").permitAll() .requestMatchers(POST, "/api/v1/users").hasRole("TEMP_USER") diff --git a/src/main/java/org/chzz/market/common/util/StringCaseConverter.java b/src/main/java/org/chzz/market/common/util/StringCaseConverter.java new file mode 100644 index 00000000..c12d04a7 --- /dev/null +++ b/src/main/java/org/chzz/market/common/util/StringCaseConverter.java @@ -0,0 +1,32 @@ +package org.chzz.market.common.util; + +public class StringCaseConverter { + + /** + * 문자열을 대문자로 변환하고, '-'를 '_'로 바꿈 예: "example-string" -> "EXAMPLE_STRING" + * 요청에서 Enum값을 매칭할때 클라이언트에선 소문자와 하이푼을 쓰기 때문에 변환 + * + * @param source 변환할 입력 문자열 + * @return 변환된 문자열 + */ + public static String toUpperCaseWithUnderscores(String source) { + if (source == null) { + return null; + } + return source.trim().toUpperCase().replace("-", "_"); + } + + /** + * 문자열을 소문자로 변환하고, '_'를 '-'로 바꿈 예: "EXAMPLE_STRING" -> "example-string" + * Enum값을 내보낼때, 클라이언트에선 소문자와 하이푼을 쓰기 때문에 변환 + * + * @param source 변환할 입력 문자열 + * @return 변환된 문자열 + */ + public static String toLowerCaseWithHyphens(String source) { + if (source == null) { + return null; + } + return source.trim().toLowerCase().replace("_", "-"); + } +} diff --git a/src/main/java/org/chzz/market/common/util/StringToEnumConverterFactory.java b/src/main/java/org/chzz/market/common/util/StringToEnumConverterFactory.java index 9b667da9..4f30bb81 100644 --- a/src/main/java/org/chzz/market/common/util/StringToEnumConverterFactory.java +++ b/src/main/java/org/chzz/market/common/util/StringToEnumConverterFactory.java @@ -23,7 +23,7 @@ public T convert(String source) { if (source == null || source.isEmpty()) { return null; } - return (T) Enum.valueOf(this.enumType, source.trim().toUpperCase().replace("-", "_")); + return (T) Enum.valueOf(this.enumType, StringCaseConverter.toUpperCaseWithUnderscores(source)); } } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java index 2baa0855..22b48613 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java @@ -12,7 +12,6 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; @@ -57,8 +56,8 @@ ResponseEntity getWinningBid(@LoginUser Long userId, @Operation(summary = "특정 경매 전환", description = "특정 사전 경매를 정식 경매로 전환합니다.") @PostMapping("/start") - ResponseEntity startAuction(@LoginUser Long userId, - @PathVariable Long auctionId); + ResponseEntity startAuction(@LoginUser Long userId, + @PathVariable Long auctionId); @Operation(summary = "특정 경매 좋아요(찜) 요청 및 취소", description = "특정 경매에 대한 좋아요(찜) 요청 및 취소를 합니다.") @PostMapping("/likes") diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java index 1a712904..d64e6957 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java @@ -2,9 +2,9 @@ import java.util.Map; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.service.AuctionDeleteService; +import org.chzz.market.domain.auctionv2.service.AuctionStartService; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.like.dto.LikeResponse; import org.chzz.market.domain.product.dto.UpdateProductRequest; @@ -15,10 +15,11 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -//@RestController +@RestController @RequiredArgsConstructor public class AuctionDetailController implements AuctionDetailApi { private final AuctionDeleteService auctionDeleteService; + private final AuctionStartService auctionStartService; @Override public ResponseEntity getAuctionDetails(Long userId, Long auctionId) { @@ -36,8 +37,9 @@ public ResponseEntity getWinningBid(Long userId, Long } @Override - public ResponseEntity startAuction(Long userId, Long auctionId) { - return null; + public ResponseEntity startAuction(Long userId, Long auctionId) { + auctionStartService.start(userId, auctionId); + return ResponseEntity.ok().build(); } @Override diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java index 2f245aec..1fdb201f 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java @@ -7,10 +7,10 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; import org.chzz.market.domain.auction.dto.response.RegisterResponse; +import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; import org.chzz.market.domain.auctionv2.dto.view.AuctionType; import org.chzz.market.domain.auctionv2.dto.view.UserAuctionType; -import org.chzz.market.domain.product.dto.CategoryResponse; -import org.chzz.market.domain.product.entity.Product.Category; +import org.chzz.market.domain.auctionv2.entity.Category; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java index 0a846092..54c2cfce 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java @@ -1,20 +1,25 @@ package org.chzz.market.domain.auctionv2.controller; import java.util.List; +import lombok.RequiredArgsConstructor; import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; import org.chzz.market.domain.auction.dto.response.RegisterResponse; +import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; import org.chzz.market.domain.auctionv2.dto.view.AuctionType; import org.chzz.market.domain.auctionv2.dto.view.UserAuctionType; -import org.chzz.market.domain.product.dto.CategoryResponse; -import org.chzz.market.domain.product.entity.Product.Category; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.service.AuctionCategoryService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -//@RestController +@RestController +@RequiredArgsConstructor public class AuctionV2Controller implements AuctionV2Api { + private final AuctionCategoryService auctionCategoryService; + @Override public ResponseEntity> getAuctionList(Long userId, Category category, AuctionType type, Pageable pageable) { return null; @@ -22,7 +27,7 @@ public ResponseEntity> getAuctionList(Long userId, Category category, Au @Override public ResponseEntity> getCategoryList() { - return null; + return ResponseEntity.ok(auctionCategoryService.getCategories()); } @Override diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java new file mode 100644 index 00000000..fd543eb7 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java @@ -0,0 +1,8 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CategoryResponse( + @Schema(description = "카테고리 값", example = "fashion-and-clothing") String code, + @Schema(description = "카테고리 이름", example = "패션 및 의류") String displayName) { +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java index 8591de44..8ea7039d 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java @@ -1,5 +1,8 @@ package org.chzz.market.domain.auctionv2.entity; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; + import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -22,10 +25,15 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.chzz.market.domain.auction.entity.listener.AuctionEntityListener; +import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.base.entity.BaseTimeEntity; import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.imagev2.error.ImageErrorCode; +import org.chzz.market.domain.imagev2.error.exception.ImageException; import org.chzz.market.domain.user.entity.User; +import org.hibernate.annotations.DynamicUpdate; // TODO: V2 경매 API 전환이 끝나서 운영 환경에 적용할 땐 기존 테이블에서 데이터를 이관해야 합니다.(flyway 스크립트) @Table(name = "auction_v2") @@ -33,8 +41,10 @@ @EntityListeners(value = AuctionEntityListener.class) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Builder +@DynamicUpdate @AllArgsConstructor @Getter +@Slf4j public class AuctionV2 extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -78,14 +88,16 @@ public class AuctionV2 extends BaseTimeEntity { @OneToMany(mappedBy = "auction", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval = true) private List images = new ArrayList<>(); - public boolean isNowOwner(Long userId) { - return !isOwner(userId); - } - public boolean isOwner(Long userId) { return seller.getId().equals(userId); } + public void validateOwner(Long userId) { + if (!isOwner(userId)) { + throw new AuctionException(AUCTION_ACCESS_FORBIDDEN); + } + } + public boolean isPreAuction() { return status == AuctionStatus.PRE; } @@ -93,4 +105,22 @@ public boolean isPreAuction() { public boolean isOfficialAuction() { return status == AuctionStatus.PROCEEDING || status == AuctionStatus.ENDED; } + + public void startOfficialAuction() { + if (isOfficialAuction()) { + throw new AuctionException(AUCTION_ALREADY_OFFICIAL); + } + this.status = AuctionStatus.PROCEEDING; + } + + public String getFirstImageCdnPath() { + return images.stream() + .filter(image -> image.getSequence() == 1) + .map(ImageV2::getCdnPath) + .findFirst() + .orElseThrow(() -> { + log.error("경매의 첫 번째 이미지가 없는 경우: {}", this.id); + return new ImageException(ImageErrorCode.IMAGE_NOT_FOUND); + }); + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java index b3018c92..a7b90339 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java @@ -1,5 +1,6 @@ package org.chzz.market.domain.auctionv2.error; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.FORBIDDEN; import static org.springframework.http.HttpStatus.NOT_FOUND; @@ -11,6 +12,7 @@ @Getter @AllArgsConstructor public enum AuctionErrorCode implements ErrorCode { + AUCTION_ALREADY_OFFICIAL(BAD_REQUEST, "해당 경매는 이미 정식 경매입니다."), OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), AUCTION_NOT_FOUND(NOT_FOUND, "경매를 찾을 수 없습니다."); @@ -19,6 +21,7 @@ public enum AuctionErrorCode implements ErrorCode { private final String message; public static class Const { + public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; public static final String AUCTION_NOT_FOUND = "AUCTION_NOT_FOUND"; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java new file mode 100644 index 00000000..5be2988d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java @@ -0,0 +1,26 @@ +package org.chzz.market.domain.auctionv2.service; + +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.util.StringCaseConverter; +import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuctionCategoryService { + + /** + * 경매의 카테고리를 조회 + */ + public List getCategories() { + return Arrays.stream(Category.values()) + .map(category -> new CategoryResponse(StringCaseConverter.toLowerCaseWithHyphens(category.name()), + category.getDisplayName())) + .toList(); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java index 25b1b504..7f1e2965 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java @@ -41,9 +41,7 @@ public void delete(Long userId, Long auctionId) { * 경매 취소 유효성 검사 */ private static void validate(Long userId, AuctionV2 auction) { - if (auction.isNowOwner(userId)) { - throw new AuctionException(AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN); - } + auction.validateOwner(userId); if (auction.isOfficialAuction()) { throw new AuctionException(AuctionErrorCode.OFFICIAL_AUCTION_DELETE_FORBIDDEN); } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java new file mode 100644 index 00000000..153ca2d8 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java @@ -0,0 +1,52 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_START; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.notification.event.NotificationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class AuctionStartService { + private final AuctionV2Repository auctionV2Repository; + private final LikeV2Repository likeRepository; + private final ApplicationEventPublisher eventPublisher; + + /** + * 사전 등록 상품 경매 전환 처리 + */ + @Transactional + public void start(Long userId, Long auctionId) { + AuctionV2 auction = auctionV2Repository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + auction.validateOwner(userId); + auction.startOfficialAuction(); + processStartNotification(auction); + log.info("{}번 경매 정식경매 전환완료", auctionId); + } + + private void processStartNotification(AuctionV2 auction) { + // 1. 해당 경매에 좋아요 누른 사용자 ID 추출 + List likedUserIds = likeRepository.findByAuctionId(auction.getId()).stream().map(like -> like.getUserId()) + .toList(); + + // 2. 경매 시작 알림 이벤트 발행 + if (!likedUserIds.isEmpty()) { + eventPublisher.publishEvent(NotificationEvent.createAuctionNotification(likedUserIds, AUCTION_START, + AUCTION_START.getMessage(auction.getName()), + auction.getFirstImageCdnPath(), auction.getId())); + } + } +} diff --git a/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java b/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java index 5bc86d44..cb882a96 100644 --- a/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java +++ b/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java @@ -8,12 +8,14 @@ @Getter @AllArgsConstructor public enum ImageErrorCode implements ErrorCode { - IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다."); + IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다."), + IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지가 존재하지 않습니다."); private final HttpStatus httpStatus; private final String message; public static class Const { public static final String IMAGE_DELETE_FAILED = "IMAGE_DELETE_FAILED"; + public static final String IMAGE_NOT_FOUND = "IMAGE_NOT_FOUND"; } } diff --git a/src/test/java/org/chzz/market/common/util/StringCaseConverterTest.java b/src/test/java/org/chzz/market/common/util/StringCaseConverterTest.java new file mode 100644 index 00000000..7f035e7a --- /dev/null +++ b/src/test/java/org/chzz/market/common/util/StringCaseConverterTest.java @@ -0,0 +1,34 @@ +package org.chzz.market.common.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class StringCaseConverterTest { + + @Test + void 대문자로변환_하이픈을언더스코어로변환() { + // given + String input = "example-string"; + String expectedOutput = "EXAMPLE_STRING"; + + // when + String result = StringCaseConverter.toUpperCaseWithUnderscores(input); + + // then + assertThat(result).isEqualTo(expectedOutput); + } + + @Test + void 소문자로변환_언더스코어를하이픈으로변환() { + // given + String input = "EXAMPLE_STRING"; + String expectedOutput = "example-string"; + + // when + String result = StringCaseConverter.toLowerCaseWithHyphens(input); + + // then + assertThat(result).isEqualTo(expectedOutput); + } +} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java b/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java new file mode 100644 index 00000000..387116cc --- /dev/null +++ b/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java @@ -0,0 +1,91 @@ +package org.chzz.market.domain.auctionv2.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.imagev2.error.ImageErrorCode.IMAGE_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.imagev2.error.exception.ImageException; +import org.chzz.market.domain.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AuctionV2Test { + private static final String ERROR_CODE = "errorCode"; + + private AuctionV2 auction; + private User owner; + private User otherUser; + + @BeforeEach + void setUp() { + owner = User.builder() + .id(1L) + .build(); + otherUser = User.builder() + .id(2L) + .build(); + + auction = AuctionV2.builder() + .seller(owner) + .status(AuctionStatus.PRE) + .build(); + } + + @Test + void 소유자가_맞는경우_예외가_발생하지않는다() { + assertDoesNotThrow(() -> auction.validateOwner(owner.getId())); + } + + @Test + void 소유자가_아닌경우_예외가_발생한다() { + // 소유자가 아닌 경우 - 예외 발생 + assertThatThrownBy(() -> auction.validateOwner(otherUser.getId())) + .isInstanceOf(AuctionException.class) + .extracting(ERROR_CODE) + .isEqualTo(AUCTION_ACCESS_FORBIDDEN); + } + + @Test + void 정식경매_전환성공() { + assertThat(auction.getStatus()).isEqualTo(AuctionStatus.PRE); + auction.startOfficialAuction(); + assertThat(auction.getStatus()).isEqualTo(AuctionStatus.PROCEEDING); + } + + @Test + void 전환할때_이미정식경매인_경우() { + auction.startOfficialAuction(); + assertThatThrownBy(auction::startOfficialAuction) + .isInstanceOf(AuctionException.class) + .extracting(ERROR_CODE) + .isEqualTo(AUCTION_ALREADY_OFFICIAL); + } + + @Test + void 첫번째이미지를_정상적으로_반환한다() { + ImageV2 firstImage = ImageV2.builder() + .cdnPath("cdn/path/to/first_image.jpg") + .sequence(1) + .build(); + ImageV2 secondImage = ImageV2.builder() + .cdnPath("cdn/path/to/second_image.jpg") + .sequence(2) + .build(); + auction.getImages().addAll(List.of(firstImage, secondImage)); + assertThat(auction.getFirstImageCdnPath()).isEqualTo("cdn/path/to/first_image.jpg"); + } + + @Test + void 예상치_못한_오류로_경매의_이미지가_없을시_예외가_발생한다() { + assertThatThrownBy(auction::getFirstImageCdnPath) + .isInstanceOf(ImageException.class) + .extracting(ERROR_CODE) + .isEqualTo(IMAGE_NOT_FOUND); + } +} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java index c387da6f..73aa8064 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java +++ b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java @@ -5,6 +5,8 @@ import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.OFFICIAL_AUCTION_DELETE_FORBIDDEN; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -49,7 +51,7 @@ class AuctionDeleteServiceTest { LikeV2 like = mock(LikeV2.class); when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); - when(auction.isNowOwner(any())).thenReturn(false); + doNothing().when(auction).validateOwner(any()); when(auction.isOfficialAuction()).thenReturn(false); when(likeRepository.findByAuctionId(auction.getId())).thenReturn(List.of(like)); @@ -68,7 +70,7 @@ class AuctionDeleteServiceTest { AuctionV2 auction = mock(AuctionV2.class); when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); - when(auction.isNowOwner(any())).thenReturn(false); + doNothing().when(auction).validateOwner(any()); when(auction.isOfficialAuction()).thenReturn(false); when(likeRepository.findByAuctionId(auction.getId())).thenReturn(List.of()); @@ -100,7 +102,7 @@ class AuctionDeleteServiceTest { // when when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); - when(auction.isNowOwner(any())).thenReturn(true); + doThrow(new AuctionException(AUCTION_ACCESS_FORBIDDEN)).when(auction).validateOwner(any()); // then assertThatThrownBy(() -> auctionDeleteService.delete(1L, 1L)) @@ -116,7 +118,7 @@ class AuctionDeleteServiceTest { // when when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); - when(auction.isNowOwner(any())).thenReturn(false); + doNothing().when(auction).validateOwner(any()); when(auction.isOfficialAuction()).thenReturn(true); // then diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java new file mode 100644 index 00000000..18ce7995 --- /dev/null +++ b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java @@ -0,0 +1,72 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.likev2.entity.LikeV2; +import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.notification.event.NotificationEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@ExtendWith(MockitoExtension.class) +class AuctionStartServiceTest { + @Mock + private AuctionV2Repository auctionRepository; + + @Mock + private LikeV2Repository likeRepository; + + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private AuctionStartService auctionStartService; + + @Test + public void 사전경매에서_정식경매로_전환_성공() { + // given + AuctionV2 auction = mock(AuctionV2.class); + LikeV2 like = mock(LikeV2.class); + + when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); + doNothing().when(auction).validateOwner(any()); + doNothing().when(auction).startOfficialAuction(); + when(likeRepository.findByAuctionId(auction.getId())).thenReturn(List.of(like)); + + // when + auctionStartService.start(1L, 1L); + + // then + verify(eventPublisher, times(1)).publishEvent(any(NotificationEvent.class)); + } + + @Test + public void 사전경매에서_정식경매로_전환할때_좋아요가_없을시_알림이벤트발행을_하지않는다() { + // given + AuctionV2 auction = mock(AuctionV2.class); + + when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); + doNothing().when(auction).validateOwner(any()); + doNothing().when(auction).startOfficialAuction(); + when(likeRepository.findByAuctionId(auction.getId())).thenReturn(List.of()); + + // when + auctionStartService.start(1L, 1L); + + // then + verify(eventPublisher, times(0)).publishEvent(any(NotificationEvent.class)); + } +} From 1c090a8fd366acf92b7c7df456a5a109c940ad02 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:27:59 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=9E=85=EC=B0=B0=20=EC=A1=B0=ED=9A=8C,=20=EA=B2=BD=EB=A7=A4?= =?UTF-8?q?=20=EB=82=99=EC=B0=B0=20=EC=A1=B0=ED=9A=8C,=20=EA=B2=BD?= =?UTF-8?q?=EB=A7=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20API=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20(#122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: isWinner 메서드 추가 * fix: 불필요한 entity 필드 검증 로직 삭제 * feat: 낙찰자가 아닐때 에러코드 추가 * feat: 낙찰자 정보 조회 쿼리 추가 * feat: 낙찰 정보 조회 api 전환 * feat: 낙찰 정보 조회 응답 객체 추가 * feat: 낙찰 정보 조회 서비스 함수 추가 * test: 낙찰 정보 조회 쿼리 테스트 추가 * test: 낙찰 정보 서비스 함수 테스트 추가 * test: 낙찰자 검증 함수 테스트 추가 * docs: api 에러 응답 문서화 추가 * feat: 경매의 입찰 내역 조회 API 전 * feat: 아직 경매가 끝나지 않을때 발생하는 에러코드 추가 * feat: 아직 경매가 끊나는걸 검사하는 도메인 함수 추가 * feat: 경매의 입찰 내역 정보 조회 쿼리 추가 * feat: 경매의 입찰 내역 정보 조회 서비스 함수 추가 * test: 경매의 입찰 내역 정보 조회 쿼리 테스트 추가 * test: 경매가 끝나는지 확인하는 함수 테스트 추가 * feat: 테스트 함수 서비스 전환 * feat: 경매 테스트 함수 전환 * refactor: 컨트롤러에 매핑정보 추가 * test: @BeforeAll 에서 @BeforeEach로 수정 * test: @Transactional 어노테이션 누락 추가 * test: @Transactional 어노테이션 누락 추가 * test: 테스트코드 수정 * refactor: 조건 명확하게 수정 --- .../controller/AuctionDetailApi.java | 45 ++++++---- .../controller/AuctionDetailController.java | 38 ++++++-- .../auctionv2/controller/AuctionV2Api.java | 2 +- .../controller/AuctionV2Controller.java | 35 ++++++-- .../response/WonAuctionDetailsResponse.java | 14 +++ .../domain/auctionv2/entity/AuctionV2.java | 28 ++++-- .../auctionv2/error/AuctionErrorCode.java | 4 + .../repository/AuctionV2QueryRepository.java | 38 ++++++++ .../auctionv2/service/AuctionTestService.java | 66 ++++++++++++++ .../auctionv2/service/AuctionWonService.java | 36 ++++++++ .../bid/repository/BidQueryRepository.java | 54 ++++++++++++ .../domain/bid/service/BidLookupService.java | 33 +++++++ .../chzz/market/domain/user/entity/User.java | 2 - .../AuctionRepositoryCustomImplTest.java | 76 +++++++++------- .../auctionv2/entity/AuctionV2Test.java | 39 ++++++++ .../AuctionV2QueryRepositoryTest.java | 56 ++++++++++++ .../service/AuctionWonServiceTest.java | 65 ++++++++++++++ .../repository/BidQueryRepositoryTest.java | 88 +++++++++++++++++++ 18 files changed, 649 insertions(+), 70 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java create mode 100644 src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java create mode 100644 src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java create mode 100644 src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java create mode 100644 src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java create mode 100644 src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java index 22b48613..32befe7d 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java @@ -1,7 +1,10 @@ package org.chzz.market.domain.auctionv2.controller; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_ENDED; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NOW_WINNER; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.OFFICIAL_AUCTION_DELETE_FORBIDDEN; import static org.chzz.market.domain.imagev2.error.ImageErrorCode.Const.IMAGE_DELETE_FAILED; @@ -12,7 +15,7 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.imagev2.error.ImageErrorCode; @@ -22,50 +25,59 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @Tag(name = "auctions(v2)", description = "V2 경매 API") -@RequestMapping("/v2/auctions/{auctionId}") public interface AuctionDetailApi { - @Operation(summary = "특정 경매 상세 조회", description = "특정 경매 상세 정보를 조회합니다.") // TODO: 정식경매와 사전 경매 응답 구분 추가 - @GetMapping + @Operation(summary = "특정 경매 상세 조회", description = "특정 경매 상세 정보를 조회합니다.") + // TODO: 정식경매와 사전 경매 응답 구분 추가 ResponseEntity getAuctionDetails(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 입찰 목록 조회", description = "특정 경매 입찰 목록을 조회합니다.") - @GetMapping("/bids") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_ENDED, name = "아직 경매가 끝나지 않을때"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ACCESS_FORBIDDEN, name = "경매의 접근 권한이 없는 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), + } + ) ResponseEntity> getBids(@LoginUser Long userId, @PathVariable Long auctionId, - @ParameterObject @PageableDefault Pageable pageable); // TODO: 내림차순 디폴트 + @ParameterObject @PageableDefault(sort = "bid-amount", direction = Sort.Direction.DESC) Pageable pageable); @Operation(summary = "특정 경매 낙찰 조회", description = "특정 경매 낙찰 정보를 조회합니다.") - @GetMapping("/won") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = NOW_WINNER, name = "낙찰자가 아닐때"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), + } + ) ResponseEntity getWinningBid(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 전환", description = "특정 사전 경매를 정식 경매로 전환합니다.") - @PostMapping("/start") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ALREADY_OFFICIAL, name = "이미 정식 경매 인 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ACCESS_FORBIDDEN, name = "경매의 접근 권한이 없는 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), + } + ) ResponseEntity startAuction(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 좋아요(찜) 요청 및 취소", description = "특정 경매에 대한 좋아요(찜) 요청 및 취소를 합니다.") - @PostMapping("/likes") ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 수정", description = "특정 경매를 수정합니다.") - @PatchMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) ResponseEntity updateAuction(@LoginUser Long userId, @PathVariable Long auctionId, @RequestPart @Valid UpdateProductRequest request, @@ -80,7 +92,6 @@ ResponseEntity updateAuction(@LoginUser Long userId, @ApiExceptionExplanation(value = ImageErrorCode.class, constant = IMAGE_DELETE_FAILED, name = "이미지 삭제에 실패한 경우"), } ) - @DeleteMapping ResponseEntity deleteAuction(@LoginUser Long userId, @PathVariable Long auctionId); } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java index d64e6957..ed39ce1c 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java @@ -1,53 +1,78 @@ package org.chzz.market.domain.auctionv2.controller; +import static org.springframework.data.domain.Sort.Direction.DESC; + import java.util.Map; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.common.config.LoginUser; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.service.AuctionDeleteService; import org.chzz.market.domain.auctionv2.service.AuctionStartService; +import org.chzz.market.domain.auctionv2.service.AuctionWonService; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.service.BidLookupService; import org.chzz.market.domain.like.dto.LikeResponse; import org.chzz.market.domain.product.dto.UpdateProductRequest; import org.chzz.market.domain.product.dto.UpdateProductResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor +@RequestMapping("/v2/auctions/{auctionId}") public class AuctionDetailController implements AuctionDetailApi { private final AuctionDeleteService auctionDeleteService; private final AuctionStartService auctionStartService; + private final AuctionWonService auctionWonService; + private final BidLookupService bidLookupService; @Override - public ResponseEntity getAuctionDetails(Long userId, Long auctionId) { + @GetMapping + public ResponseEntity getAuctionDetails(@LoginUser Long userId, @PathVariable Long auctionId) { return null; } @Override - public ResponseEntity> getBids(Long userId, Long auctionId, Pageable pageable) { - return null; + @GetMapping("/bids") + public ResponseEntity> getBids(@LoginUser Long userId, + @PathVariable Long auctionId, + @PageableDefault(sort = "bid-amount", direction = DESC) Pageable pageable) { + return ResponseEntity.ok(bidLookupService.getBidsByAuctionId(userId, auctionId, pageable)); } @Override - public ResponseEntity getWinningBid(Long userId, Long auctionId) { - return null; + @GetMapping("/won") + public ResponseEntity getWinningBid(@LoginUser Long userId, + @PathVariable Long auctionId) { + return ResponseEntity.ok(auctionWonService.getWinningBidByAuctionId(userId, auctionId)); } @Override + @PostMapping("/start") public ResponseEntity startAuction(Long userId, Long auctionId) { auctionStartService.start(userId, auctionId); return ResponseEntity.ok().build(); } @Override + @PostMapping("/likes") public ResponseEntity likeAuction(Long userId, Long auctionId) { return null; } @Override + @PatchMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity updateAuction(Long userId, Long auctionId, UpdateProductRequest request, Map images) { @@ -55,6 +80,7 @@ public ResponseEntity updateAuction(Long userId, Long auc } @Override + @DeleteMapping public ResponseEntity deleteAuction(Long userId, Long auctionId) { auctionDeleteService.delete(userId, auctionId); return ResponseEntity.ok().build(); diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java index 1fdb201f..f16548c3 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java @@ -50,5 +50,5 @@ ResponseEntity registerAuction(@LoginUser Long userId, @Operation(summary = "경매 테스트 등록", description = "테스트 등록합니다.") @PostMapping("/test") ResponseEntity testEndAuction(@LoginUser Long userId, - @RequestParam int seconds); + @RequestParam int seconds); } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java index 54c2cfce..43fed9d2 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java @@ -1,7 +1,9 @@ package org.chzz.market.domain.auctionv2.controller; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; +import org.chzz.market.common.config.LoginUser; import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; import org.chzz.market.domain.auction.dto.response.RegisterResponse; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; @@ -9,9 +11,16 @@ import org.chzz.market.domain.auctionv2.dto.view.UserAuctionType; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.auctionv2.service.AuctionCategoryService; +import org.chzz.market.domain.auctionv2.service.AuctionTestService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -19,30 +28,44 @@ @RequiredArgsConstructor public class AuctionV2Controller implements AuctionV2Api { private final AuctionCategoryService auctionCategoryService; + private final AuctionTestService testService; @Override - public ResponseEntity> getAuctionList(Long userId, Category category, AuctionType type, Pageable pageable) { + @GetMapping + public ResponseEntity> getAuctionList(@LoginUser Long userId, + @RequestParam(required = false) Category category, + @RequestParam AuctionType type, + @PageableDefault(sort = "newest") Pageable pageable) { return null; } @Override + @GetMapping("/categories") public ResponseEntity> getCategoryList() { return ResponseEntity.ok(auctionCategoryService.getCategories()); } @Override - public ResponseEntity> getUserAuctionList(Long userId, UserAuctionType type, Pageable pageable) { + @GetMapping("/users") + public ResponseEntity> getUserAuctionList(@LoginUser Long userId, + @RequestParam UserAuctionType type, + @PageableDefault(sort = "newest") Pageable pageable) { return null; } @Override - public ResponseEntity registerAuction(Long userId, BaseRegisterRequest request, - List images) { + @PostMapping + public ResponseEntity registerAuction(@LoginUser Long userId, + @RequestPart("request") @Valid BaseRegisterRequest request, + @RequestPart(value = "images") List images) { return null; } @Override - public ResponseEntity testEndAuction(Long userId, int seconds) { - return null; + @PostMapping("/test") + public ResponseEntity testEndAuction(@LoginUser Long userId, + @RequestParam("seconds") int seconds) { + testService.test(userId, seconds); + return ResponseEntity.status(HttpStatus.CREATED).build(); } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java new file mode 100644 index 00000000..7b78c445 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java @@ -0,0 +1,14 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; + +public record WonAuctionDetailsResponse( + Long auctionId, + String productName, + String imageUrl, + Long winningAmount +) { + @QueryProjection + public WonAuctionDetailsResponse { + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java index 8ea7039d..5ed42982 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java @@ -2,11 +2,11 @@ import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_ENDED; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; @@ -26,7 +26,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auction.entity.listener.AuctionEntityListener; import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.base.entity.BaseTimeEntity; import org.chzz.market.domain.image.entity.ImageV2; @@ -38,11 +37,11 @@ // TODO: V2 경매 API 전환이 끝나서 운영 환경에 적용할 땐 기존 테이블에서 데이터를 이관해야 합니다.(flyway 스크립트) @Table(name = "auction_v2") @Entity -@EntityListeners(value = AuctionEntityListener.class) +//@EntityListeners(value = AuctionEntityListener.class) +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @Builder @DynamicUpdate -@AllArgsConstructor @Getter @Slf4j public class AuctionV2 extends BaseTimeEntity { @@ -78,16 +77,23 @@ public class AuctionV2 extends BaseTimeEntity { @Column private Long winnerId; + @Builder.Default @Column - private Integer likeCount; + private Integer likeCount = 0; + @Builder.Default @Column - private Integer bidCount; + private Integer bidCount = 0; @Builder.Default @OneToMany(mappedBy = "auction", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval = true) private List images = new ArrayList<>(); + public void addImage(ImageV2 image) { + images.add(image); + image.specifyAuction(this); + } + public boolean isOwner(Long userId) { return seller.getId().equals(userId); } @@ -106,6 +112,16 @@ public boolean isOfficialAuction() { return status == AuctionStatus.PROCEEDING || status == AuctionStatus.ENDED; } + public void validateAuctionEnded() { + if (!status.equals(AuctionStatus.ENDED)) { + throw new AuctionException(AUCTION_NOT_ENDED); + } + } + + public boolean isWinner(Long userId) { + return winnerId != null && winnerId.equals(userId); + } + public void startOfficialAuction() { if (isOfficialAuction()) { throw new AuctionException(AUCTION_ALREADY_OFFICIAL); diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java index a7b90339..946c6480 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java @@ -12,8 +12,10 @@ @Getter @AllArgsConstructor public enum AuctionErrorCode implements ErrorCode { + AUCTION_NOT_ENDED(BAD_REQUEST, "해당 경매가 아직 끝나지 않았습니다."), AUCTION_ALREADY_OFFICIAL(BAD_REQUEST, "해당 경매는 이미 정식 경매입니다."), OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), + NOW_WINNER(FORBIDDEN, "낙찰자가 아닙니다."), AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), AUCTION_NOT_FOUND(NOT_FOUND, "경매를 찾을 수 없습니다."); @@ -21,8 +23,10 @@ public enum AuctionErrorCode implements ErrorCode { private final String message; public static class Const { + public static final String AUCTION_NOT_ENDED = "AUCTION_NOT_ENDED"; public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; + public static final String NOW_WINNER = "NOW_WINNER"; public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; public static final String AUCTION_NOT_FOUND = "AUCTION_NOT_FOUND"; } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java new file mode 100644 index 00000000..45302dfc --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java @@ -0,0 +1,38 @@ +package org.chzz.market.domain.auctionv2.repository; + +import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; +import static org.chzz.market.domain.bid.entity.QBid.bid; +import static org.chzz.market.domain.image.entity.QImageV2.imageV2; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.response.QWonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class AuctionV2QueryRepository { + private final JPAQueryFactory jpaQueryFactory; + + /** + * 낙찰자 정보 조회 + */ + public Optional findWinningBidById(Long auctionId) { + return Optional.ofNullable(jpaQueryFactory.select( + new QWonAuctionDetailsResponse(auctionV2.id, auctionV2.name, imageV2.cdnPath, bid.amount)) + .from(auctionV2) + .leftJoin(bid).on(bid.bidderId.eq(auctionV2.winnerId) + .and(bid.auctionId.eq(auctionV2.id))) + .leftJoin(imageV2).on(isRepresentativeImage()) + .where(auctionV2.id.eq(auctionId)) + .fetchOne()); + } + + private BooleanExpression isRepresentativeImage() { + return imageV2.auction.eq(auctionV2).and(imageV2.sequence.eq(1)); + } + +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java new file mode 100644 index 00000000..768aa022 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java @@ -0,0 +1,66 @@ +package org.chzz.market.domain.auctionv2.service; + +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.auction.type.AuctionStatus; +import org.chzz.market.domain.image.entity.Image; +import org.chzz.market.domain.image.repository.ImageRepository; +import org.chzz.market.domain.product.entity.Product; +import org.chzz.market.domain.product.entity.Product.Category; +import org.chzz.market.domain.product.repository.ProductRepository; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.stereotype.Service; + +/** + * 경매종료 테스트 서비스 삭제필요 + */ +@Service +@RequiredArgsConstructor +public class AuctionTestService { + private final AuctionRepository auctionRepository; + private final ProductRepository productRepository; + private final ImageRepository imageRepository; + private final UserRepository userRepository; + + @Transactional + public void test(Long userId, int seconds) { + Random random = new Random(); + int randomIndex = random.nextInt(1000) + 1; // 1부터 1000까지 랜덤 숫자 생성 + int randomIndex1 = random.nextInt(1000) + 1; // 1부터 1000까지 랜덤 숫자 생성 + User user = userRepository.findById(userId).get(); + Product product = Product.builder() + .name("테스트" + randomIndex) + .description("test") + .category(Category.ELECTRONICS) + .user(user) + .minPrice(10000) + .build(); + productRepository.save(product); + + Image image1 = Image.builder() + .cdnPath("https://picsum.photos/id/" + randomIndex + "/200/200") + .product(product) + .sequence(1) + .build(); + + Image image2 = Image.builder() + .cdnPath("https://picsum.photos/id/" + randomIndex1 + "/200/200") + .product(product) + .sequence(2) + .build(); + imageRepository.save(image1); + imageRepository.save(image2); + product.addImages(List.of(image1, image2)); + auctionRepository.save(Auction.builder() + .status(AuctionStatus.PROCEEDING) + .endDateTime(LocalDateTime.now().plusSeconds(seconds)) + .product(product) + .build()); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java new file mode 100644 index 00000000..e843bbf0 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java @@ -0,0 +1,36 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.chzz.market.common.error.GlobalErrorCode.RESOURCE_NOT_FOUND; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.NOW_WINNER; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.error.GlobalException; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuctionWonService { + private final AuctionV2Repository auctionRepository; + private final AuctionV2QueryRepository auctionQueryRepository; + + /** + * 낙찰 정보 조회 + */ + public WonAuctionDetailsResponse getWinningBidByAuctionId(Long userId, Long auctionId) { + AuctionV2 auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + if (!auction.isWinner(userId)) { + throw new AuctionException(NOW_WINNER); + } + return auctionQueryRepository.findWinningBidById(auctionId) + .orElseThrow(() -> new GlobalException(RESOURCE_NOT_FOUND)); + } +} diff --git a/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java b/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java new file mode 100644 index 00000000..489ddf19 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java @@ -0,0 +1,54 @@ +package org.chzz.market.domain.bid.repository; + +import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; +import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; +import static org.chzz.market.domain.bid.entity.QBid.bid; +import static org.chzz.market.domain.user.entity.QUser.user; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.util.QuerydslOrderProvider; +import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.dto.response.QBidInfoResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class BidQueryRepository { + private final JPAQueryFactory jpaQueryFactory; + private final QuerydslOrderProvider querydslOrderProvider; + + /** + * 특정 경매의 입찰 조회 + */ + public Page findBidsByAuctionId(Long auctionId, Pageable pageable) { + BooleanExpression isWinner = auctionV2.winnerId.isNotNull().and(auctionV2.winnerId.eq(user.id)); + + JPAQuery baseQuery = jpaQueryFactory.from(bid) + .join(auctionV2).on(bid.auctionId.eq(auctionV2.id) + .and(auctionV2.id.eq(auctionId)) + .and(bid.status.eq(ACTIVE))); + + List content = baseQuery + .select(new QBidInfoResponse( + bid.amount, + user.nickname, + isWinner + )) + .join(user).on(bid.bidderId.eq(user.id)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery. + select(bid.count()); + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } +} diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java b/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java new file mode 100644 index 00000000..313cee69 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java @@ -0,0 +1,33 @@ +package org.chzz.market.domain.bid.service; + +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.repository.BidQueryRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class BidLookupService { + private final AuctionV2Repository auctionRepository; + private final BidQueryRepository bidQueryRepository; + + public Page getBidsByAuctionId(Long userId, Long auctionId, Pageable pageable) { + AuctionV2 auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + if (!auction.isOwner(userId)) { + throw new AuctionException(AUCTION_ACCESS_FORBIDDEN); + } + auction.validateAuctionEnded(); + return bidQueryRepository.findBidsByAuctionId(auctionId, pageable); + } +} diff --git a/src/main/java/org/chzz/market/domain/user/entity/User.java b/src/main/java/org/chzz/market/domain/user/entity/User.java index 487c32ab..da22cff5 100644 --- a/src/main/java/org/chzz/market/domain/user/entity/User.java +++ b/src/main/java/org/chzz/market/domain/user/entity/User.java @@ -14,7 +14,6 @@ import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; -import jakarta.validation.constraints.Email; import java.util.UUID; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -47,7 +46,6 @@ public class User extends BaseTimeEntity { private String nickname; @Column(nullable = false) - @Email(message = "invalid type of email") private String email; @Column(columnDefinition = "TEXT") diff --git a/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java b/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java index 838bd809..1c2489c5 100644 --- a/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java +++ b/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java @@ -42,7 +42,6 @@ import org.chzz.market.domain.user.dto.response.ParticipationCountsResponse; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -61,22 +60,22 @@ class AuctionRepositoryCustomImplTest { @Autowired AuctionRepository auctionRepository; - private static User user1, user2, user3, user4, user5; - private static Product product1, product2, product3, product4, product5, product6, product7, product8, product9, product10; - private static Auction auction1, auction2, auction3, auction4, auction5, auction6, auction7, auction8, auction9, auction10; - - private static Image image1, image2, image3, image4, image5, image6; - private static Bid bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8, bid9, bid10, bid11, bid12, bid13, bid14, bid15; - private static Order order1; - - @BeforeAll - static void setUpOnce(@Autowired UserRepository userRepository, - @Autowired ProductRepository productRepository, - @Autowired AuctionRepository auctionRepository, - @Autowired ImageRepository imageRepository, - @Autowired BidRepository bidRepository, - @Autowired PaymentRepository paymentRepository, - @Autowired OrderRepository orderRepository) { + private User user1, user2, user3, user4, user5; + private Product product1, product2, product3, product4, product5, product6, product7, product8, product9, product10; + private Auction auction1, auction2, auction3, auction4, auction5, auction6, auction7, auction8, auction9, auction10; + + private Image image1, image2, image3, image4, image5, image6; + private Bid bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8, bid9, bid10, bid11, bid12, bid13, bid14, bid15; + private Order order1; + + @BeforeEach + void init(@Autowired UserRepository userRepository, + @Autowired ProductRepository productRepository, + @Autowired AuctionRepository auctionRepository, + @Autowired ImageRepository imageRepository, + @Autowired BidRepository bidRepository, + @Autowired PaymentRepository paymentRepository, + @Autowired OrderRepository orderRepository) { user1 = User.builder().providerId("1234").nickname("닉네임1").email("asd@naver.com").build(); user2 = User.builder().providerId("12345").nickname("닉네임2").email("asd1@naver.com").build(); user3 = User.builder().providerId("123456").nickname("닉네임3").email("asd12@naver.com").build(); @@ -151,19 +150,32 @@ static void setUpOnce(@Autowired UserRepository userRepository, image6 = Image.builder().product(product8).cdnPath("path/to/image5.jpg").sequence(1).build(); imageRepository.saveAll(List.of(image1, image2, image3, image4, image5, image6)); - bid1 = Bid.builder().bidderId(user2.getId()).auctionId(auction1.getId()).amount(2000L).build(); - bid2 = Bid.builder().bidderId(user2.getId()).auctionId(auction2.getId()).amount(4000L).build(); - bid3 = Bid.builder().bidderId(user1.getId()).auctionId(auction3.getId()).amount(5000L).build(); - bid4 = Bid.builder().bidderId(user3.getId()).auctionId(auction2.getId()).amount(6000L).build(); - bid5 = Bid.builder().bidderId(user1.getId()).auctionId(auction5.getId()).amount(7000L).build(); - bid6 = Bid.builder().bidderId(user2.getId()).auctionId(auction6.getId()).amount(8000L).build(); - bid7 = Bid.builder().bidderId(user3.getId()).auctionId(auction3.getId()).amount(310000L).build(); - bid8 = Bid.builder().bidderId(user4.getId()).auctionId(auction3.getId()).amount(320000L).build(); - bid10 = Bid.builder().bidderId(user2.getId()).auctionId(auction3.getId()).amount(8000L).build(); - bid11 = Bid.builder().bidderId(user2.getId()).auctionId(auction4.getId()).amount(15000L).build(); - bid12 = Bid.builder().bidderId(user3.getId()).auctionId(auction4.getId()).amount(25000L).build(); - bid13 = Bid.builder().bidderId(user4.getId()).auctionId(auction8.getId()).amount(250000L).build(); - bid14 = Bid.builder().bidderId(user2.getId()).auctionId(auction8.getId()).amount(150000L).build(); + bid1 = Bid.builder().bidderId(user2.getId()).auctionId(auction1.getId()).status(BidStatus.ACTIVE).amount(2000L) + .build(); + bid2 = Bid.builder().bidderId(user2.getId()).auctionId(auction2.getId()).status(BidStatus.ACTIVE).amount(4000L) + .build(); + bid3 = Bid.builder().bidderId(user1.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE).amount(5000L) + .build(); + bid4 = Bid.builder().bidderId(user3.getId()).auctionId(auction2.getId()).status(BidStatus.ACTIVE).amount(6000L) + .build(); + bid5 = Bid.builder().bidderId(user1.getId()).auctionId(auction5.getId()).status(BidStatus.ACTIVE).amount(7000L) + .build(); + bid6 = Bid.builder().bidderId(user2.getId()).auctionId(auction6.getId()).status(BidStatus.ACTIVE).amount(8000L) + .build(); + bid7 = Bid.builder().bidderId(user3.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE) + .amount(310000L).build(); + bid8 = Bid.builder().bidderId(user4.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE) + .amount(320000L).build(); + bid10 = Bid.builder().bidderId(user2.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE).amount(8000L) + .build(); + bid11 = Bid.builder().bidderId(user2.getId()).auctionId(auction4.getId()).status(BidStatus.ACTIVE) + .amount(15000L).build(); + bid12 = Bid.builder().bidderId(user3.getId()).auctionId(auction4.getId()).status(BidStatus.ACTIVE) + .amount(25000L).build(); + bid13 = Bid.builder().bidderId(user4.getId()).auctionId(auction8.getId()).status(BidStatus.ACTIVE) + .amount(250000L).build(); + bid14 = Bid.builder().bidderId(user2.getId()).auctionId(auction8.getId()).status(BidStatus.ACTIVE) + .amount(150000L).build(); bid15 = Bid.builder().bidderId(user5.getId()).auctionId(auction9.getId()).amount(75000L) .status(BidStatus.ACTIVE).build(); @@ -192,7 +204,7 @@ public void testFindAuctionsByCategoryExpensive() throws Exception { //when Page result = auctionRepository.findAuctionsByCategory( - Category.FASHION_AND_CLOTHING, 1L, pageable); + Category.FASHION_AND_CLOTHING, user1.getId(), pageable); //then assertThat(result).isNotNull(); @@ -215,7 +227,7 @@ public void testFindAuctionsByCategoryPopularity() throws Exception { //when Page result = auctionRepository.findAuctionsByCategory( - Category.FASHION_AND_CLOTHING, 2L, pageable); + Category.FASHION_AND_CLOTHING, user2.getId(), pageable); //then assertThat(result).isNotNull(); diff --git a/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java b/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java index 387116cc..8d7a187f 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java +++ b/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_ENDED; import static org.chzz.market.domain.imagev2.error.ImageErrorCode.IMAGE_NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -88,4 +89,42 @@ void setUp() { .extracting(ERROR_CODE) .isEqualTo(IMAGE_NOT_FOUND); } + + @Test + void 낙찰자가_맞는경우() { + AuctionV2 winnerAuction = AuctionV2.builder() + .seller(owner) + .status(AuctionStatus.PRE) + .winnerId(owner.getId()) + .build(); + assertThat(winnerAuction.isWinner(owner.getId())).isTrue(); + } + + @Test + void 낙찰자가_null_일때_조회하는경우_false_반환() { + assertThat(auction.isWinner(1L)).isFalse(); + } + + @Test + void 낙찰자가_아닐때_조회하는경우_false_반환() { + AuctionV2 winnerAuction = AuctionV2.builder() + .seller(owner) + .status(AuctionStatus.PRE) + .winnerId(owner.getId()) + .build(); + assertThat(winnerAuction.isWinner(owner.getId() + 1)).isFalse(); + } + + @Test + void 아직_경매가_끝나지_않을때_예외가_발생한다() { + List auctions = List.of( + AuctionV2.builder().seller(owner).status(AuctionStatus.PRE).winnerId(owner.getId()).build(), + AuctionV2.builder().seller(owner).status(AuctionStatus.PROCEEDING).winnerId(owner.getId()).build() + ); + + auctions.forEach(auction -> assertThatThrownBy(auction::validateAuctionEnded) + .isInstanceOf(AuctionException.class) + .extracting(ERROR_CODE) + .isEqualTo(AUCTION_NOT_ENDED)); + } } diff --git a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java new file mode 100644 index 00000000..8d1f97ea --- /dev/null +++ b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java @@ -0,0 +1,56 @@ +package org.chzz.market.domain.auctionv2.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.repository.BidRepository; +import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.entity.User.ProviderType; +import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +class AuctionV2QueryRepositoryTest { + @Autowired + private AuctionV2QueryRepository auctionQueryRepository; + @Autowired + private AuctionV2Repository auctionV2Repository; + @Autowired + private BidRepository bidRepository; + @Autowired + private UserRepository userRepository; + + @Test + void 낙찰정보를_조회한다() { + // given + ImageV2 imageV2 = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + + User user = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + userRepository.save(user); + AuctionV2 auction = AuctionV2.builder().seller(user).name("맥북프로").description("맥북프로 2019년형 팝니다.") + .status(AuctionStatus.PROCEEDING).category(Category.ELECTRONICS).winnerId(1L).build(); + auction.addImage(imageV2); + Bid bid1 = Bid.builder().bidderId(1L).auctionId(1L).amount(2000L).build(); + Bid bid2 = Bid.builder().bidderId(2L).auctionId(1L).amount(1000L).build(); + auctionV2Repository.save(auction); + bidRepository.saveAll(List.of(bid1, bid2)); + // when + Optional result = auctionQueryRepository.findWinningBidById(1L); + + // then + assertThat(result).isPresent(); + assertThat(result.get().winningAmount()).isEqualTo(2000L); + } + +} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java new file mode 100644 index 00000000..c368ede9 --- /dev/null +++ b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java @@ -0,0 +1,65 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.chzz.market.common.error.GlobalErrorCode.RESOURCE_NOT_FOUND; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.NOW_WINNER; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.chzz.market.common.error.GlobalException; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AuctionWonServiceTest { + @InjectMocks + private AuctionWonService auctionWonService; + + @Mock + private AuctionV2Repository auctionV2Repository; + + @Mock + private AuctionV2QueryRepository auctionV2QueryRepository; + + @Test + void 낙찰자가_아니거나_낙찰자가_존재하지않는데_조회하면_에러가_발생한다() { + // given + AuctionV2 auction = AuctionV2.builder().winnerId(1L).build(); + Long userId = 2L; + Long auctionId = 1L; + + //when + when(auctionV2Repository.findById(any())).thenReturn(Optional.of(auction)); + // then + assertThatThrownBy(() -> auctionWonService.getWinningBidByAuctionId(userId, auctionId)) + .isInstanceOf(AuctionException.class) + .extracting("errorCode") + .isEqualTo(NOW_WINNER); + } + + @Test + void 예기치못한_에러로_낙찰정보가_조회되지_않으면_에러가_발생한다() { + // given + AuctionV2 auction = AuctionV2.builder().winnerId(1L).build(); + Long userId = 1L; + Long auctionId = 1L; + + //when + when(auctionV2Repository.findById(any())).thenReturn(Optional.of(auction)); + when(auctionV2QueryRepository.findWinningBidById(any())).thenReturn(Optional.empty()); + // then + assertThatThrownBy(() -> auctionWonService.getWinningBidByAuctionId(userId, auctionId)) + .isInstanceOf(GlobalException.class) + .extracting("errorCode") + .isEqualTo(RESOURCE_NOT_FOUND); + } + +} diff --git a/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java new file mode 100644 index 00000000..f33df148 --- /dev/null +++ b/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java @@ -0,0 +1,88 @@ +package org.chzz.market.domain.bid.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Comparator; +import java.util.List; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.entity.Bid.BidStatus; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.entity.User.ProviderType; +import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +class BidQueryRepositoryTest { + @Autowired + AuctionV2Repository auctionV2Repository; + + @Autowired + BidRepository bidRepository; + @Autowired + UserRepository userRepository; + + @Autowired + BidQueryRepository bidQueryRepository; + + @Test + void 해당경매_입찰내역을_조회한다() { + User owner = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + User user1 = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + User user2 = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + User user3 = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + User user4 = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + userRepository.saveAll(List.of(owner, user1, user2, user3, user4)); + + AuctionV2 auction = AuctionV2.builder().seller(owner).name("맥북프로").description("맥북프로 2019년형 팝니다.") + .status(AuctionStatus.ENDED).category(Category.ELECTRONICS).winnerId(user1.getId()).build(); + auctionV2Repository.save(auction); + Bid bid1 = Bid.builder().bidderId(user1.getId()).auctionId(auction.getId()).amount(2000L) + .status(BidStatus.ACTIVE).build(); + Bid bid2 = Bid.builder().bidderId(user2.getId()).auctionId(auction.getId()).amount(1000L) + .status(BidStatus.ACTIVE).build(); + Bid bid3 = Bid.builder().bidderId(user3.getId()).auctionId(auction.getId()).amount(1000L) + .status(BidStatus.ACTIVE).build(); + Bid bid4 = Bid.builder().bidderId(user4.getId()).auctionId(auction.getId()).amount(3000L) + .status(BidStatus.CANCELLED).build(); + bidRepository.saveAll(List.of(bid1, bid2, bid3, bid4)); + Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "bid-amount")); + Page result = bidQueryRepository.findBidsByAuctionId(auction.getId(), pageable); + List content = result.getContent(); + + System.out.println("content = " + content); + // then + assertThat(content).hasSize(3); + assertThat(content).isSortedAccordingTo( + Comparator.comparing(BidInfoResponse::bidAmount).reversed()); + assertThat(content.get(0).bidAmount()).isEqualTo(2000L); + assertThat(content.get(0).isWinningBidder()).isTrue(); + assertThat(content.get(1).isWinningBidder()).isFalse(); + } + + @Test + void 해당경매_입찰내역이_아무것도_없을때_조회한다() { + User owner = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + userRepository.save(owner); + AuctionV2 auction = AuctionV2.builder().seller(owner).name("맥북프로").description("맥북프로 2019년형 팝니다.") + .status(AuctionStatus.PROCEEDING).category(Category.ELECTRONICS).winnerId(null).build(); + auctionV2Repository.save(auction); + Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "bid-amount")); + Page result = bidQueryRepository.findBidsByAuctionId(auction.getId(), pageable); + List content = result.getContent(); + // then + assertThat(content).hasSize(0); + } +} From eab10512bc7289e0620a7ccf7df84761fe918ff5 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:48:21 +0900 Subject: [PATCH 03/16] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=A0=84=ED=99=98=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 좋아요수, 입찰 수 Long 타입으로 변경 * feat: 경매 상세 조회 API 추가 * feat: 경매 상태를 찾는 쿼리 추가 * feat: 경매 상세정보 조회 쿼리 추가 * chore: Spring security API 인증 허용 * feat: 경매 상세 정보 조회 추상클래스 추가 * feat: 사전 경매 상세 조회 응답 객체 추가 * feat: 정식 경매 상세정보 조회 응답 객체 추가 * feat: 전환을 위한 임시 V2 Entity 추가 * feat: 전환을 위한 임시 V2 Entity 스크립트 추가 * feat: 경매 상세 정보 조회 서비스 함수 추가 * feat: V2 Payment Method 추가 * feat: V2 Payment Method 추가 * test: 경매 상세정보 조회 테스트 추가 * refactor: 명확한 클래스명으로 수정 - AuctionDetailBaseResponse -> BaseAuctionDetailResponse --- .../market/common/config/SecurityConfig.java | 3 +- .../controller/AuctionDetailApi.java | 12 +- .../controller/AuctionDetailController.java | 7 +- .../response/BaseAuctionDetailResponse.java | 41 ++++ .../OfficialAuctionDetailResponse.java | 54 +++++ .../response/PreAuctionDetailResponse.java | 26 +++ .../domain/auctionv2/entity/AuctionV2.java | 4 +- .../repository/AuctionV2QueryRepository.java | 128 +++++++++++ .../repository/AuctionV2Repository.java | 5 + .../service/AuctionDetailService.java | 37 +++ .../market/domain/orderv2/entity/OrderV2.java | 74 ++++++ .../orderv2/repository/OrderV2Repository.java | 7 + .../domain/paymentv2/entity/PaymentV2.java | 79 +++++++ .../domain/paymentv2/entity/Status.java | 12 + .../respository/PaymentV2Repository.java | 7 + .../V16__create_v2_order_payment.sql | 55 +++++ .../AuctionV2QueryRepositoryTest.java | 210 ++++++++++++++++-- 17 files changed, 739 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java create mode 100644 src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java create mode 100644 src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java create mode 100644 src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java create mode 100644 src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java create mode 100644 src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java create mode 100644 src/main/resources/db/migration/V16__create_v2_order_payment.sql diff --git a/src/main/java/org/chzz/market/common/config/SecurityConfig.java b/src/main/java/org/chzz/market/common/config/SecurityConfig.java index 89422033..6b1ba1df 100644 --- a/src/main/java/org/chzz/market/common/config/SecurityConfig.java +++ b/src/main/java/org/chzz/market/common/config/SecurityConfig.java @@ -72,7 +72,8 @@ public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception "/api/v1/users/*", "/api/v1/users/check/nickname/*").permitAll() .requestMatchers(GET, - "/api/v2/auctions/categories").permitAll() + "/api/v2/auctions/categories", + "/api/v2/auctions/{auctionId:\\d+}").permitAll() .requestMatchers(POST, "/api/v1/users/tokens/reissue").permitAll() .requestMatchers(POST, "/api/v1/users").hasRole("TEMP_USER") diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java index 32befe7d..9adc23c4 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java @@ -9,12 +9,18 @@ import static org.chzz.market.domain.imagev2.error.ImageErrorCode.Const.IMAGE_DELETE_FAILED; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.Map; import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; +import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; @@ -27,6 +33,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @@ -36,7 +43,10 @@ @Tag(name = "auctions(v2)", description = "V2 경매 API") public interface AuctionDetailApi { @Operation(summary = "특정 경매 상세 조회", description = "특정 경매 상세 정보를 조회합니다.") - // TODO: 정식경매와 사전 경매 응답 구분 추가 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정식경매 응답", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = OfficialAuctionDetailResponse.class))), + @ApiResponse(responseCode = "201", description = "사전경매 응답", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = PreAuctionDetailResponse.class))), + }) ResponseEntity getAuctionDetails(@LoginUser Long userId, @PathVariable Long auctionId); diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java index ed39ce1c..9c546e79 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java @@ -7,6 +7,7 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.service.AuctionDeleteService; +import org.chzz.market.domain.auctionv2.service.AuctionDetailService; import org.chzz.market.domain.auctionv2.service.AuctionStartService; import org.chzz.market.domain.auctionv2.service.AuctionWonService; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; @@ -32,6 +33,7 @@ @RequiredArgsConstructor @RequestMapping("/v2/auctions/{auctionId}") public class AuctionDetailController implements AuctionDetailApi { + private final AuctionDetailService auctionDetailService; private final AuctionDeleteService auctionDeleteService; private final AuctionStartService auctionStartService; private final AuctionWonService auctionWonService; @@ -39,8 +41,9 @@ public class AuctionDetailController implements AuctionDetailApi { @Override @GetMapping - public ResponseEntity getAuctionDetails(@LoginUser Long userId, @PathVariable Long auctionId) { - return null; + public ResponseEntity getAuctionDetails(@LoginUser Long userId, + @PathVariable Long auctionId) { + return ResponseEntity.ok(auctionDetailService.getAuctionDetails(userId, auctionId)); } @Override diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java new file mode 100644 index 00000000..71feea38 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java @@ -0,0 +1,41 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.image.dto.ImageResponse; + +@Getter +@NoArgsConstructor +public abstract class BaseAuctionDetailResponse { + private Long auctionId; + private String sellerNickname; + private String sellerProfileImageUrl; + private String productName; + private String description; + private Integer minPrice; + protected Boolean isSeller; + private AuctionStatus status; + private Category category; + private List images; + + public BaseAuctionDetailResponse(Long auctionId, String sellerNickname, String sellerProfileImageUrl, + String productName, String description, Integer minPrice, Boolean isSeller, + AuctionStatus status, Category category) { + this.auctionId = auctionId; + this.sellerNickname = sellerNickname; + this.sellerProfileImageUrl = sellerProfileImageUrl; + this.productName = productName; + this.description = description; + this.minPrice = minPrice; + this.isSeller = isSeller; + this.status = status; + this.category = category; + } + + public void addImageList(List images) { + this.images = images; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java new file mode 100644 index 00000000..282a0bdf --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java @@ -0,0 +1,54 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.Category; + +@Getter +@NoArgsConstructor +public class OfficialAuctionDetailResponse extends BaseAuctionDetailResponse { + private Long timeRemaining; + private Long participantCount; + private Boolean isParticipated; + private Long bidId; + private Long bidAmount; + private int remainingBidCount; + private Boolean isCancelled; + @Schema(description = "낙찰자인지 여부") + private Boolean isWinner; + @Schema(description = "낙찰되었는지 여부") + private Boolean isWon; + @Schema(description = "주문 여부 - 판매자와 낙찰자에게만 제공") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean isOrdered; + + public OfficialAuctionDetailResponse(Long auctionId, String sellerNickname, String sellerProfileImageUrl, + String productName, String description, Integer minPrice, Boolean isSeller, + AuctionStatus status, Category category, Long timeRemaining, + Long participantCount, Boolean isParticipated, Long bidId, Long bidAmount, + int remainingBidCount, Boolean isCancelled, Boolean isWinner, Boolean isWon, + Boolean isOrdered) { + super(auctionId, sellerNickname, sellerProfileImageUrl, productName, description, minPrice, isSeller, status, + category); + this.timeRemaining = timeRemaining; + this.participantCount = participantCount; + this.isParticipated = isParticipated; + this.bidId = bidId; + this.bidAmount = bidAmount; + this.remainingBidCount = remainingBidCount; + this.isCancelled = isCancelled; + this.isWinner = isWinner; + this.isWon = isWon; + this.isOrdered = isOrdered; + } + + public OfficialAuctionDetailResponse clearOrderIfNotEligible() { + if (!isSeller && !isWinner) { + this.isOrdered = null; + } + return this; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java new file mode 100644 index 00000000..48aeff6d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java @@ -0,0 +1,26 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.Category; + +@Getter +@NoArgsConstructor +public class PreAuctionDetailResponse extends BaseAuctionDetailResponse { + private LocalDateTime updatedAt; + private Long likeCount; + private Boolean isLiked; + + public PreAuctionDetailResponse(Long auctionId, String sellerNickname, String sellerProfileImageUrl, + String productName, + String description, Integer minPrice, Boolean isSeller, AuctionStatus status, + Category category, LocalDateTime updatedAt, Long likeCount, Boolean isLiked) { + super(auctionId, sellerNickname, sellerProfileImageUrl, productName, description, minPrice, isSeller, status, + category); + this.updatedAt = updatedAt; + this.likeCount = likeCount; + this.isLiked = isLiked; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java index 5ed42982..7672005b 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java @@ -79,11 +79,11 @@ public class AuctionV2 extends BaseTimeEntity { @Builder.Default @Column - private Integer likeCount = 0; + private Long likeCount = 0L; @Builder.Default @Column - private Integer bidCount = 0; + private Long bidCount = 0L; @Builder.Default @OneToMany(mappedBy = "auction", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval = true) diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java index 45302dfc..007bad49 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java @@ -1,15 +1,32 @@ package org.chzz.market.domain.auctionv2.repository; +import static com.querydsl.core.types.dsl.Expressions.numberTemplate; +import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; +import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; +import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; import static org.chzz.market.domain.bid.entity.QBid.bid; import static org.chzz.market.domain.image.entity.QImageV2.imageV2; +import static org.chzz.market.domain.likev2.entity.QLikeV2.likeV2; +import static org.chzz.market.domain.orderv2.entity.QOrderV2.orderV2; +import static org.chzz.market.domain.user.entity.QUser.user; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.QWonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.bid.entity.QBid; +import org.chzz.market.domain.image.dto.ImageResponse; +import org.chzz.market.domain.image.dto.QImageResponse; import org.springframework.stereotype.Repository; @Repository @@ -31,8 +48,119 @@ public Optional findWinningBidById(Long auctionId) { .fetchOne()); } + public Optional findPreAuctionDetailById(Long userId, Long auctionId) { + Optional result = Optional.ofNullable(jpaQueryFactory + .select( + Projections.constructor( + PreAuctionDetailResponse.class, + auctionV2.id, + user.nickname, + user.profileImageUrl, + auctionV2.name, + auctionV2.description, + auctionV2.minPrice, + userIdEq(userId), + auctionV2.status, + auctionV2.category, + auctionV2.updatedAt, + auctionV2.likeCount, + isAuctionLikedByUserId(userId) + ) + ) + .from(auctionV2) + .join(auctionV2.seller, user) + .where(auctionV2.id.eq(auctionId)) + .fetchOne()); + + result.ifPresent(response -> response.addImageList(getImagesByAuctionId(response.getAuctionId()))); + return result; + } + + public Optional findOfficialAuctionDetailById(Long userId, Long auctionId) { + QBid activeBid = new QBid("bidActive"); + QBid canceledBid = new QBid("bidCanceled"); + Optional officialAuctionDetailResponse = Optional.ofNullable(jpaQueryFactory + .select( + Projections.constructor( + OfficialAuctionDetailResponse.class, + auctionV2.id, + user.nickname, + user.profileImageUrl, + auctionV2.name, + auctionV2.description, + auctionV2.minPrice, + userIdEq(userId), + auctionV2.status, + auctionV2.category, + timeRemaining().longValue(), + auctionV2.bidCount, + activeBid.id.isNotNull(), + activeBid.id, + activeBid.amount.coalesce(0L), + activeBid.count.coalesce(3), + canceledBid.id.isNotNull(), + winnerIdEq(userId), + auctionV2.winnerId.isNotNull(), + orderV2.isNotNull() + ) + ) + .from(auctionV2) + .join(auctionV2.seller, user) + .leftJoin(activeBid).on(activeBid.auctionId.eq(auctionId) // 활성화된 입찰 조인 + .and(activeBid.status.eq(ACTIVE)) + .and(bidderIdEqSub(activeBid, userId))) + .leftJoin(canceledBid).on(canceledBid.auctionId.eq(auctionId) // 취소된 입찰 조인 + .and(canceledBid.status.eq(CANCELLED)) + .and(bidderIdEqSub(canceledBid, userId))) + .leftJoin(orderV2).on(orderV2.auction.eq(auctionV2)) + .where(auctionV2.id.eq(auctionId)) + .fetchOne()); + + officialAuctionDetailResponse.ifPresent( + response -> response.addImageList(getImagesByAuctionId(response.getAuctionId()))); + + return officialAuctionDetailResponse; + } + + private List getImagesByAuctionId(Long auctionId) { + return jpaQueryFactory + .select(new QImageResponse(imageV2.id, imageV2.cdnPath)) + .from(imageV2) + .where(imageV2.auction.id.eq(auctionId)) + .orderBy(imageV2.sequence.asc()) + .fetch(); + } + + private BooleanExpression isAuctionLikedByUserId(Long userId) { + return JPAExpressions.selectOne() + .from(likeV2) + .where(likeV2.auctionId.eq(auctionV2.id) + .and(likeUserIdEq(userId))) + .exists(); + } + private BooleanExpression isRepresentativeImage() { return imageV2.auction.eq(auctionV2).and(imageV2.sequence.eq(1)); } + private BooleanBuilder userIdEq(Long userId) { + return nullSafeBuilder(() -> user.id.eq(userId)); + } + + private BooleanBuilder bidderIdEqSub(QBid qBid, Long userId) { + return nullSafeBuilder(() -> qBid.bidderId.eq(userId)); + } + + private BooleanBuilder winnerIdEq(Long userId) { + return nullSafeBuilder(() -> auctionV2.winnerId.isNotNull().and(auctionV2.winnerId.eq(userId))); + } + + private BooleanBuilder likeUserIdEq(Long userId) { + return nullSafeBuilder(() -> likeV2.userId.eq(userId)); + } + + private static NumberExpression timeRemaining() { + return numberTemplate(Integer.class, + "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java index 7e07549a..67550640 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java @@ -1,8 +1,13 @@ package org.chzz.market.domain.auctionv2.repository; +import java.util.Optional; import org.chzz.market.domain.auction.repository.AuctionRepositoryCustom; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.AuctionV2; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface AuctionV2Repository extends JpaRepository, AuctionRepositoryCustom { + @Query("SELECT a.status FROM AuctionV2 a WHERE a.id = :auctionId") + Optional findAuctionStatusById(Long auctionId); } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java new file mode 100644 index 00000000..cc9af7af --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java @@ -0,0 +1,37 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.response.BaseAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AuctionDetailService { + private final AuctionV2Repository auctionRepository; + private final AuctionV2QueryRepository auctionQueryRepository; + + public BaseAuctionDetailResponse getAuctionDetails(Long userId, Long auctionId) { + return auctionRepository.findAuctionStatusById(auctionId) + .flatMap(status -> getAuctionDetailByStatus(status, userId, auctionId)) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + } + + private Optional getAuctionDetailByStatus(AuctionStatus status, Long userId, + Long auctionId) { + return switch (status) { + case PRE -> auctionQueryRepository.findPreAuctionDetailById(userId, auctionId) + .map(response -> response); + case PROCEEDING, ENDED -> auctionQueryRepository.findOfficialAuctionDetailById(userId, auctionId) + .map(response -> response.clearOrderIfNotEligible()); + }; + } +} diff --git a/src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java b/src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java new file mode 100644 index 00000000..bce00afe --- /dev/null +++ b/src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java @@ -0,0 +1,74 @@ +package org.chzz.market.domain.orderv2.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.paymentv2.entity.PaymentV2.PaymentMethod; + +@Entity +@Getter +@Builder +@Table(name = "orders_v2") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class OrderV2 { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "order_id") + private Long id; + + @Column(nullable = false) + private String orderNo; + + @Column(nullable = false) + private Long buyerId; + + @Column(nullable = false) + private Long paymentId; + + @Column(nullable = false) + private Long amount; + + @Column + private String deliveryMemo; + + @Column(nullable = false) + private String roadAddress; + + @Column(nullable = false) + private String jibun; + + @Column(nullable = false) + private String zipcode; + + @Column(nullable = false) + private String detailAddress; + + @Column(nullable = false) + private String recipientName; + + @Column(nullable = false) + private String phoneNumber; + + @Column(columnDefinition = "varchar(30)", nullable = false) + @Enumerated(EnumType.STRING) + private PaymentMethod method; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id") + private AuctionV2 auction; +} diff --git a/src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java b/src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java new file mode 100644 index 00000000..88d84c31 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java @@ -0,0 +1,7 @@ +package org.chzz.market.domain.orderv2.repository; + +import org.chzz.market.domain.orderv2.entity.OrderV2; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrderV2Repository extends JpaRepository { +} diff --git a/src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java b/src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java new file mode 100644 index 00000000..1dacbe10 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java @@ -0,0 +1,79 @@ +package org.chzz.market.domain.paymentv2.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.base.entity.BaseTimeEntity; +import org.chzz.market.domain.user.entity.User; + +@Getter +@Entity +@Table +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PaymentV2 extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "payment_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User payer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id", nullable = false) + private AuctionV2 auction; + + @Column(nullable = false) + private Long amount; + + @Column(columnDefinition = "varchar(30)", nullable = false) + @Enumerated(EnumType.STRING) + private PaymentMethod method; + + @Column(columnDefinition = "varchar(30)", nullable = false) + @Enumerated(EnumType.STRING) + private Status status; + + @Column(unique = true, nullable = false) + private String orderNo; + + @Column(nullable = false) + private String paymentKey; + + @PrePersist + protected void onPrePersist() { + if (this.status == null) { + this.status = Status.READY; + } + } + + @AllArgsConstructor + public enum PaymentMethod { + CARD("카드"), + VIRTUAL_ACCOUNT("가상계좌"), + EASY_PAYMENT("간편결제"), + MOBILE("휴대폰"), + ACCOUNT_TRANSFER("계좌이체"), + CULTURE_GIFT_CARD("문화상품권"), + BOOK_CULTURE_GIFT_CARD("도서문화상품권"), + GAME_CULTURE_GIFT_CARD("게임문화상품권"), + CASH("테스트용"); + + private final String description; + } +} diff --git a/src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java b/src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java new file mode 100644 index 00000000..b17ea897 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java @@ -0,0 +1,12 @@ +package org.chzz.market.domain.paymentv2.entity; + +public enum Status { + READY, + IN_PROGRESS, + WAITING_FOR_DEPOSIT, + DONE, + CANCELED, + PARTIAL_CANCELED, + ABORTED, + EXPIRED +} diff --git a/src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java b/src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java new file mode 100644 index 00000000..79dc0d34 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java @@ -0,0 +1,7 @@ +package org.chzz.market.domain.paymentv2.respository; + +import org.chzz.market.domain.paymentv2.entity.PaymentV2; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentV2Repository extends JpaRepository { +} diff --git a/src/main/resources/db/migration/V16__create_v2_order_payment.sql b/src/main/resources/db/migration/V16__create_v2_order_payment.sql new file mode 100644 index 00000000..cc9ae76e --- /dev/null +++ b/src/main/resources/db/migration/V16__create_v2_order_payment.sql @@ -0,0 +1,55 @@ +-- 파일명: V16__create_v2_order_payment.sql +-- 파일 설명: v2 order, payment, 경매 API 전환완료시 삭제 예정 +-- 작성일: 2024-11-18 +-- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. +-- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. + +CREATE TABLE orders_v2 +( + order_id BIGINT AUTO_INCREMENT NOT NULL, + order_no VARCHAR(255) NOT NULL, + buyer_id BIGINT NOT NULL, + payment_id BIGINT NOT NULL, + amount BIGINT NOT NULL, + delivery_memo VARCHAR(255) NULL, + road_address VARCHAR(255) NOT NULL, + jibun VARCHAR(255) NOT NULL, + zipcode VARCHAR(255) NOT NULL, + detail_address VARCHAR(255) NOT NULL, + recipient_name VARCHAR(255) NOT NULL, + phone_number VARCHAR(255) NOT NULL, + method VARCHAR(30) NOT NULL, + auction_id BIGINT NULL, + CONSTRAINT pk_orders_v2 PRIMARY KEY (order_id) +); + +CREATE TABLE paymentv2 +( + payment_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + user_id BIGINT NOT NULL, + auction_id BIGINT NOT NULL, + amount BIGINT NOT NULL, + method VARCHAR(30) NOT NULL, + status VARCHAR(30) NOT NULL, + order_no VARCHAR(255) NOT NULL, + payment_key VARCHAR(255) NOT NULL, + CONSTRAINT pk_paymentv2 PRIMARY KEY (payment_id) +); + +ALTER TABLE paymentv2 + ADD CONSTRAINT uc_paymentv2_orderno UNIQUE (order_no); + +ALTER TABLE orders_v2 + ADD CONSTRAINT FK_ORDERS_V2_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction_v2 (auction_id); + +ALTER TABLE paymentv2 + ADD CONSTRAINT FK_PAYMENTV2_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction_v2 (auction_id); + +ALTER TABLE paymentv2 + ADD CONSTRAINT FK_PAYMENTV2_ON_USER FOREIGN KEY (user_id) REFERENCES users (user_id); + +ALTER TABLE auction_v2 + MODIFY COLUMN bid_count BIGINT NULL, + MODIFY COLUMN like_count BIGINT NULL; diff --git a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java index 8d1f97ea..32cc9bf2 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.List; import java.util.Optional; +import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.AuctionV2; @@ -11,9 +11,14 @@ import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.repository.BidRepository; import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.orderv2.entity.OrderV2; +import org.chzz.market.domain.orderv2.repository.OrderV2Repository; +import org.chzz.market.domain.paymentv2.entity.PaymentV2.PaymentMethod; import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.entity.User.ProviderType; import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -30,27 +35,200 @@ class AuctionV2QueryRepositoryTest { private BidRepository bidRepository; @Autowired private UserRepository userRepository; + @Autowired + private OrderV2Repository orderRepository; - @Test - void 낙찰정보를_조회한다() { - // given - ImageV2 imageV2 = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + private User seller; + private User user; + private ImageV2 defaultImage; - User user = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); + @BeforeEach + void setUp() { + seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); + user = User.builder().email("user").providerId("user").providerType(User.ProviderType.KAKAO).build(); + defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + + userRepository.save(seller); userRepository.save(user); - AuctionV2 auction = AuctionV2.builder().seller(user).name("맥북프로").description("맥북프로 2019년형 팝니다.") - .status(AuctionStatus.PROCEEDING).category(Category.ELECTRONICS).winnerId(1L).build(); - auction.addImage(imageV2); - Bid bid1 = Bid.builder().bidderId(1L).auctionId(1L).amount(2000L).build(); - Bid bid2 = Bid.builder().bidderId(2L).auctionId(1L).amount(1000L).build(); + } + + private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + AuctionV2 auction = AuctionV2.builder() + .seller(seller) + .name(name) + .description(description) + .status(status) + .category(Category.ELECTRONICS) + .winnerId(winnerId) + .build(); + auction.addImage(defaultImage); auctionV2Repository.save(auction); - bidRepository.saveAll(List.of(bid1, bid2)); - // when - Optional result = auctionQueryRepository.findWinningBidById(1L); + return auction; + } + + private Bid createBid(User bidder, AuctionV2 auction, Long amount, Bid.BidStatus status) { + Bid bid = Bid.builder() + .bidderId(bidder.getId()) + .auctionId(auction.getId()) + .amount(amount) + .status(status) + .build(); + bidRepository.save(bid); + return bid; + } - // then + private OrderV2 createOrder(AuctionV2 auction, User buyer, Long amount) { + OrderV2 order = OrderV2.builder() + .auction(auction) + .buyerId(buyer.getId()) + .amount(amount) + .paymentId(1L) + .roadAddress("서울시 강남구") + .jibun("123") + .zipcode("123") + .detailAddress("123") + .recipientName("홍길동") + .phoneNumber("01012345678") + .method(PaymentMethod.CARD) + .orderNo("123") + .build(); + orderRepository.save(order); + return order; + } + + @Test + void 낙찰정보를_조회한다() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, user.getId()); + createBid(user, auction, 2000L, Bid.BidStatus.ACTIVE); + + // When + Optional result = auctionQueryRepository.findWinningBidById(auction.getId()); + + // Then assertThat(result).isPresent(); assertThat(result.get().winningAmount()).isEqualTo(2000L); } + @Nested + @DisplayName("정식 경매 상세정보조회") + class OfficialAuctionDetail { + @Test + void 본인의_제품을_조회한경우() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, seller.getId()); + + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + seller.getId(), auction.getId()); + + // Then + OfficialAuctionDetailResponse response = result.get(); + assertThat(response).isNotNull(); + assertThat(response.getIsSeller()).isTrue(); + assertThat(response.getIsParticipated()).isFalse(); + assertThat(response.getIsWon()).isTrue(); + assertThat(response.getIsOrdered()).isFalse(); + } + + @Test + void 다른_사람_경매를_참여안한경우_조회한경우() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + user.getId(), auction.getId()); + + // Then + OfficialAuctionDetailResponse response = result.get(); + assertThat(response).isNotNull(); + assertThat(response.getIsSeller()).isFalse(); + assertThat(response.getIsParticipated()).isFalse(); + } + + @Test + void 다른_사람_경매을_참여한경우_조회() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + createBid(user, auction, 1000L, Bid.BidStatus.ACTIVE); + + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + user.getId(), auction.getId()); + + // Then + OfficialAuctionDetailResponse response = result.get(); + assertThat(response).isNotNull(); + assertThat(response.getIsSeller()).isFalse(); + assertThat(response.getIsParticipated()).isTrue(); + } + + @Test + void 없는_경매_인경우() { + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + user.getId(), -1L); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void 비로그인_상태에서_조회한_경우() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + null, auction.getId()); + + // Then + OfficialAuctionDetailResponse response = result.get(); + assertThat(response).isNotNull(); + assertThat(response.getIsSeller()).isFalse(); + assertThat(response.getIsParticipated()).isFalse(); + assertThat(response.getBidId()).isNull(); + assertThat(response.getBidAmount()).isEqualTo(0L); + assertThat(response.getRemainingBidCount()).isEqualTo(3); + } + + @Test + void 취소된_입찰이_있는_경우() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + createBid(user, auction, 2000L, Bid.BidStatus.CANCELLED); + + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + user.getId(), auction.getId()); + + // Then + OfficialAuctionDetailResponse response = result.get(); + assertThat(response).isNotNull(); + assertThat(response.getIsCancelled()).isTrue(); + } + + @Test + void 주문이_있을시_조회를_한다() { + // Given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, + user.getId()); + createBid(user, auction, 2000L, Bid.BidStatus.ACTIVE); + createOrder(auction, user, 2000L); + + // When + Optional result = auctionQueryRepository.findOfficialAuctionDetailById( + user.getId(), auction.getId()); + + // Then + OfficialAuctionDetailResponse response = result.get(); + assertThat(response).isNotNull(); + assertThat(response.getIsSeller()).isFalse(); + assertThat(response.getIsParticipated()).isTrue(); + assertThat(response.getIsWon()).isTrue(); + assertThat(response.getIsWinner()).isTrue(); + assertThat(response.getIsOrdered()).isTrue(); + } + } } From 78cf70cac21a17448cbb02ce50447f2b965f9e69 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Wed, 20 Nov 2024 22:43:05 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor:=20=EC=9E=85=EC=B0=B0=20API,=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20API=20=EC=A0=84=ED=99=98=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 좋아요 API 전환 추가 * feat: 좋아요 수를 증가 하거나 감소하는 원자적 쿼리 추가 * feat: 사용자와 경매 ID의 좋아요를 찾는 쿼리 추가 * style: 불필요한 부분 제거 * feat: 좋아요 토글 함수 추가 * test: 좋아요 동시성 테스트 추가 * docs: 좋아요 API 예외 추가 * refactor: 나의 입찰 목록 조회 API 전환 * refactor: 나의 입찰 목록 조회 쿼리 추가 * refactor: 나의 입찰 목록 조회 함수 추가 * refactor: 특정 경매의 모든 Bid Entity 조회 전환 * feat: 입찰 업데이트 서비스 함수 추가 * feat: 입찰 요청 및 수정 및 취소 API 전환 * feat: 입찰 수 증가 감소 쿼리 추가 * feat: 경매 Entity에 입찰 유효성 검사 함수 추가 * feat: 경매 진행중이 아닐때 에러코드 추가 * chore: PR 리마인더 수정 * docs: README.md 프론트 배포 주소 수정 * test: 테스트 이름 수정 * style: 로그에 락 key 추가 * fix: 좋아요 분산락 적용 * test: 좋아요 분산락 동시성 테스트 추가 * fix: 입찰 생성 및 업데이트 할때도 분산락 적용 * test: 입찰 생성 및 업데이트 할때도 분산락 동시성 테스트 추가 * test: 테스트명 수정 * fix: 카운트 감소할때 0보다 클때만 조건 추가 * feat: 입찰 취소 서비스 함수 추가 * feat: 입찰 취소 분산락 서비스 함수 추가 * test: 입찰 취소 분산락 동시성 테스트 추가 * refactor: 입찰 취소 API 분리 --- .github/workflows/pr_review_reminder.yml | 1 - README.md | 4 +- .../aop/redisrock/DistributedLockAop.java | 15 +- .../controller/AuctionDetailApi.java | 10 +- .../controller/AuctionDetailController.java | 11 +- .../domain/auctionv2/entity/AuctionV2.java | 23 ++- .../auctionv2/error/AuctionErrorCode.java | 2 + .../repository/AuctionV2Repository.java | 17 ++ .../market/domain/bid/controller/BidApi.java | 34 +++- .../domain/bid/controller/BidController.java | 16 +- .../bid/repository/BidQueryRepository.java | 69 ++++++++ .../bid/service/BidCancelLockService.java | 34 ++++ .../domain/bid/service/BidCancelService.java | 30 ++++ .../domain/bid/service/BidCreateService.java | 65 +++++++ .../domain/bid/service/BidLookupService.java | 21 +++ .../likev2/repository/LikeV2Repository.java | 3 + .../likev2/service/LikeUpdateService.java | 54 ++++++ .../bid/service/BidCancelLockServiceTest.java | 162 ++++++++++++++++++ .../BidCreateServiceConcurrencyTest.java | 142 +++++++++++++++ .../LikeUpdateServiceConcurrencyTest.java | 119 +++++++++++++ 20 files changed, 795 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java create mode 100644 src/main/java/org/chzz/market/domain/bid/service/BidCancelService.java create mode 100644 src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java create mode 100644 src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java create mode 100644 src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java create mode 100644 src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java create mode 100644 src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java diff --git a/.github/workflows/pr_review_reminder.yml b/.github/workflows/pr_review_reminder.yml index d101702f..d16e34f6 100644 --- a/.github/workflows/pr_review_reminder.yml +++ b/.github/workflows/pr_review_reminder.yml @@ -63,7 +63,6 @@ jobs: run: | reviewer_map='[ {"github": "junest66", "discord": "<@444811214623735839>"}, - {"github": "viaunixue", "discord": "<@386917108455309316>"}, {"github": "YeaChan05", "discord": "<@391487793995579403>"} ]' prs=$(cat prs.json) diff --git a/README.md b/README.md index 23a035c0..5ba8bf67 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ border-radius: 20px;" >
-[✨ <치즈마켓> 사용해보기](https://chzzmarket.vercel.app/) +[✨ <치즈마켓> 사용해보기](https://chzzmarket.store/) [//]: # ([📄 API 문서 바로가기](https://app.swaggerhub.com/apis-docs/CHLWNDKS333_1/chzz-market-api/1.0.0#/Products)) @@ -186,4 +186,4 @@ border-radius: 20px;" > -
\ No newline at end of file +
diff --git a/src/main/java/org/chzz/market/common/aop/redisrock/DistributedLockAop.java b/src/main/java/org/chzz/market/common/aop/redisrock/DistributedLockAop.java index 0de96cfd..24159b3a 100644 --- a/src/main/java/org/chzz/market/common/aop/redisrock/DistributedLockAop.java +++ b/src/main/java/org/chzz/market/common/aop/redisrock/DistributedLockAop.java @@ -24,7 +24,7 @@ public class DistributedLockAop { private final RedissonClient redissonClient; private final AopForTransaction aopForTransaction; - @Around("@annotation(org.chzz.market.common.aop.redisrock.DistributedLock)") + @Around("@annotation(DistributedLock)") public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); @@ -34,28 +34,27 @@ public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable { joinPoint.getArgs(), distributedLock.key()); RLock rLock = redissonClient.getLock(key); // (1) 락의 이름으로 RLock 인스턴스를 가져옴 - log.info("Lock 획득 시도 중... [method: {}]", method.getName()); + log.debug("Lock 획득 시도 중... [method: {}, key: {}]", method.getName(), key); try { boolean available = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit()); // (2) 정의된 waitTime까지 획득을 시도, 정의된 leaseTime이 지나면 잠금을 해제 if (!available) { - log.warn("Lock 획득 실패 [method: {}]", method.getName()); + log.warn("Lock 획득 실패 [method: {}, key: {}]", method.getName(), key); return false; } - log.info("Lock 획득 성공 [method: {}]", method.getName()); + log.debug("Lock 획득 성공 [method: {}, key: {}]", method.getName(), key); return aopForTransaction.proceed(joinPoint); // (3) DistributedLock 어노테이션이 선언된 메서드를 별도의 트랜잭션으로 실행 } catch (InterruptedException e) { - log.error("Lock 획득 중 인터럽트가 발생 [method: {}]", method.getName(), e); + log.error("Lock 획득 중 인터럽트가 발생 [method: {}, key: {}]", method.getName(), key, e); throw new InterruptedException(); } finally { try { rLock.unlock(); // (4) 종료 시 무조건 락을 해제 - log.info("Lock 해제 [method: {}]", method.getName()); - + log.debug("Lock 해제 [method: {}, key: {}]", method.getName(), key); } catch (IllegalMonitorStateException e) { - log.warn("이미 Lock 해제 [method: {}]", method.getName()); + log.warn("이미 Lock 해제 [method: {}, key: {}]", method.getName(), key); } } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java index 9adc23c4..78225e84 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java @@ -25,7 +25,6 @@ import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.imagev2.error.ImageErrorCode; -import org.chzz.market.domain.like.dto.LikeResponse; import org.chzz.market.domain.product.dto.UpdateProductRequest; import org.chzz.market.domain.product.dto.UpdateProductResponse; import org.springdoc.core.annotations.ParameterObject; @@ -84,8 +83,13 @@ ResponseEntity startAuction(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 좋아요(찜) 요청 및 취소", description = "특정 경매에 대한 좋아요(찜) 요청 및 취소를 합니다.") - ResponseEntity likeAuction(@LoginUser Long userId, - @PathVariable Long auctionId); + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), + } + ) + ResponseEntity likeAuction(@LoginUser Long userId, + @PathVariable Long auctionId); @Operation(summary = "특정 경매 수정", description = "특정 경매를 수정합니다.") ResponseEntity updateAuction(@LoginUser Long userId, diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java index 9c546e79..ae65f933 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java @@ -12,7 +12,7 @@ import org.chzz.market.domain.auctionv2.service.AuctionWonService; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.bid.service.BidLookupService; -import org.chzz.market.domain.like.dto.LikeResponse; +import org.chzz.market.domain.likev2.service.LikeUpdateService; import org.chzz.market.domain.product.dto.UpdateProductRequest; import org.chzz.market.domain.product.dto.UpdateProductResponse; import org.springframework.data.domain.Page; @@ -38,6 +38,7 @@ public class AuctionDetailController implements AuctionDetailApi { private final AuctionStartService auctionStartService; private final AuctionWonService auctionWonService; private final BidLookupService bidLookupService; + private final LikeUpdateService likeUpdateService; @Override @GetMapping @@ -63,15 +64,17 @@ public ResponseEntity getWinningBid(@LoginUser Long u @Override @PostMapping("/start") - public ResponseEntity startAuction(Long userId, Long auctionId) { + public ResponseEntity startAuction(@LoginUser Long userId, + @PathVariable Long auctionId) { auctionStartService.start(userId, auctionId); return ResponseEntity.ok().build(); } @Override @PostMapping("/likes") - public ResponseEntity likeAuction(Long userId, Long auctionId) { - return null; + public ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Long auctionId) { + likeUpdateService.updateLike(userId, auctionId); + return ResponseEntity.ok().build(); } @Override diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java index 7672005b..e31fedaa 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java @@ -1,7 +1,11 @@ package org.chzz.market.domain.auctionv2.entity; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.ENDED; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PRE; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PROCEEDING; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ENDED; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_ENDED; import jakarta.persistence.CascadeType; @@ -105,15 +109,15 @@ public void validateOwner(Long userId) { } public boolean isPreAuction() { - return status == AuctionStatus.PRE; + return status == PRE; } public boolean isOfficialAuction() { - return status == AuctionStatus.PROCEEDING || status == AuctionStatus.ENDED; + return status == PROCEEDING || status == ENDED; } public void validateAuctionEnded() { - if (!status.equals(AuctionStatus.ENDED)) { + if (!status.equals(ENDED)) { throw new AuctionException(AUCTION_NOT_ENDED); } } @@ -126,7 +130,7 @@ public void startOfficialAuction() { if (isOfficialAuction()) { throw new AuctionException(AUCTION_ALREADY_OFFICIAL); } - this.status = AuctionStatus.PROCEEDING; + this.status = PROCEEDING; } public String getFirstImageCdnPath() { @@ -139,4 +143,15 @@ public String getFirstImageCdnPath() { return new ImageException(ImageErrorCode.IMAGE_NOT_FOUND); }); } + + public void validateAuctionEndTime() { + // 경매가 진행중이 아닐 때 + if (status != PROCEEDING || endDateTime == null || LocalDateTime.now().isAfter(endDateTime)) { + throw new AuctionException(AUCTION_ENDED); + } + } + + public boolean isAboveMinPrice(Long amount) { + return amount >= minPrice; + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java index 946c6480..2f18c98a 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java @@ -14,6 +14,7 @@ public enum AuctionErrorCode implements ErrorCode { AUCTION_NOT_ENDED(BAD_REQUEST, "해당 경매가 아직 끝나지 않았습니다."), AUCTION_ALREADY_OFFICIAL(BAD_REQUEST, "해당 경매는 이미 정식 경매입니다."), + AUCTION_ENDED(BAD_REQUEST, "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), NOW_WINNER(FORBIDDEN, "낙찰자가 아닙니다."), AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), @@ -25,6 +26,7 @@ public enum AuctionErrorCode implements ErrorCode { public static class Const { public static final String AUCTION_NOT_ENDED = "AUCTION_NOT_ENDED"; public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; + public static final String AUCTION_ENDED = "AUCTION_ENDED"; public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; public static final String NOW_WINNER = "NOW_WINNER"; public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java index 67550640..2dd7d4ee 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java @@ -5,9 +5,26 @@ import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.AuctionV2; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; public interface AuctionV2Repository extends JpaRepository, AuctionRepositoryCustom { @Query("SELECT a.status FROM AuctionV2 a WHERE a.id = :auctionId") Optional findAuctionStatusById(Long auctionId); + + @Modifying + @Query("UPDATE AuctionV2 a SET a.likeCount = a.likeCount + 1 WHERE a.id = :auctionId") + void incrementLikeCount(Long auctionId); + + @Modifying + @Query("UPDATE AuctionV2 a SET a.likeCount = a.likeCount - 1 WHERE a.id = :auctionId AND a.likeCount > 0") + void decrementLikeCount(Long auctionId); + + @Modifying + @Query("UPDATE AuctionV2 a SET a.bidCount = a.bidCount + 1 WHERE a.id = :auctionId") + void incrementBidCount(Long auctionId); + + @Modifying + @Query("UPDATE AuctionV2 a SET a.bidCount = a.bidCount - 1 WHERE a.id = :auctionId AND a.bidCount > 0") + void decrementBidCount(Long auctionId); } diff --git a/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java b/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java index 3649ee0d..0842d60c 100644 --- a/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java +++ b/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java @@ -1,6 +1,7 @@ package org.chzz.market.domain.bid.controller; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_ENDED; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ENDED; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; import static org.chzz.market.domain.bid.error.BidErrorCode.Const.BID_ALREADY_CANCELLED; import static org.chzz.market.domain.bid.error.BidErrorCode.Const.BID_BELOW_MIN_PRICE; import static org.chzz.market.domain.bid.error.BidErrorCode.Const.BID_BY_OWNER; @@ -11,39 +12,54 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.domain.auction.error.AuctionErrorCode; -import org.chzz.market.domain.auction.type.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.BidCreateRequest; import org.chzz.market.domain.bid.dto.query.BiddingRecord; import org.chzz.market.domain.bid.error.BidErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "bids", description = "입찰 API") public interface BidApi { @Operation(summary = "나의 입찰 목록 조회") - ResponseEntity> findUsersBidHistory(Long userId, @ParameterObject Pageable pageable, - AuctionStatus status); + ResponseEntity> findUsersBidHistory(@LoginUser Long userId, + @PageableDefault(sort = "time-remaining") @ParameterObject Pageable pageable, + @RequestParam(value = "status", required = false) AuctionStatus status); @Operation(summary = "입찰 요청 및 수정") @ApiResponseExplanations( errors = { @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_LIMIT_EXCEEDED, name = "입찰 횟수 제한을 초과 했을때"), - @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ENDED, name = "경매가 종료된 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ENDED, name = "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_SAME_AS_PREVIOUS, name = "이전 입찰금액과 동일한 경우"), @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_BELOW_MIN_PRICE, name = "입찰금액이 최소가보다 낮은 경우"), @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_BY_OWNER, name = "경매 등록자가 입찰 할때"), - @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_NOT_FOUND, name = "없는 경매 일때"), - @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_NOT_FOUND, name = "없는 경매 일때"), + @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_NOT_FOUND, name = "없는 입찰 일때"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "없는 경매 일때"), @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_ALREADY_CANCELLED, name = "취소한 입찰 일때"), } ) - ResponseEntity createBid(@Valid BidCreateRequest bidCreateRequest, Long userId); + ResponseEntity createBid(@Valid @RequestBody BidCreateRequest bidCreateRequest, + @LoginUser Long userId); @Operation(summary = "입찰 취소") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ENDED, name = "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), + @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_BY_OWNER, name = "경매 등록자가 입찰취소 할때"), + @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_NOT_FOUND, name = "없는 입찰 일때"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "없는 경매 일때"), + @ApiExceptionExplanation(value = BidErrorCode.class, constant = BID_ALREADY_CANCELLED, name = "취소한 입찰 일때"), + } + ) ResponseEntity cancelBid(Long bidId, Long userId); } diff --git a/src/main/java/org/chzz/market/domain/bid/controller/BidController.java b/src/main/java/org/chzz/market/domain/bid/controller/BidController.java index c18086a1..0a2e1e50 100644 --- a/src/main/java/org/chzz/market/domain/bid/controller/BidController.java +++ b/src/main/java/org/chzz/market/domain/bid/controller/BidController.java @@ -5,10 +5,12 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.auction.type.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.bid.dto.BidCreateRequest; import org.chzz.market.domain.bid.dto.query.BiddingRecord; -import org.chzz.market.domain.bid.service.BidService; +import org.chzz.market.domain.bid.service.BidCancelService; +import org.chzz.market.domain.bid.service.BidCreateService; +import org.chzz.market.domain.bid.service.BidLookupService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -26,7 +28,9 @@ @RequiredArgsConstructor @RequestMapping("/v1/bids") public class BidController implements BidApi { - private final BidService bidService; + private final BidLookupService bidLookupService; + private final BidCreateService bidCreateService; + private final BidCancelService bidCancelService; /** * 나의 입찰 목록 조회 @@ -42,7 +46,7 @@ public ResponseEntity> findUsersBidHistory( @LoginUser Long userId, @PageableDefault(sort = "time-remaining") Pageable pageable, @RequestParam(value = "status", required = false) AuctionStatus status) { - Page records = bidService.inquireBidHistory(userId, pageable, status); + Page records = bidLookupService.inquireBidHistory(userId, pageable, status); return ResponseEntity.ok(records); } @@ -53,7 +57,7 @@ public ResponseEntity> findUsersBidHistory( @PostMapping public ResponseEntity createBid(@Valid @RequestBody BidCreateRequest bidCreateRequest, @LoginUser Long userId) { - bidService.createBid(bidCreateRequest, userId); + bidCreateService.create(bidCreateRequest, userId); return ResponseEntity.status(CREATED).build(); } @@ -64,7 +68,7 @@ public ResponseEntity createBid(@Valid @RequestBody BidCreateRequest bidCr @PatchMapping("/{bidId}/cancel") public ResponseEntity cancelBid(@PathVariable Long bidId, @LoginUser Long userId) { - bidService.cancelBid(bidId, userId); + bidCancelService.cancel(bidId, userId); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java b/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java index 489ddf19..20790942 100644 --- a/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java +++ b/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java @@ -1,18 +1,28 @@ package org.chzz.market.domain.bid.repository; +import static com.querydsl.core.types.dsl.Expressions.numberTemplate; +import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; import static org.chzz.market.domain.bid.entity.QBid.bid; +import static org.chzz.market.domain.image.entity.QImageV2.imageV2; import static org.chzz.market.domain.user.entity.QUser.user; +import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; import org.chzz.market.common.util.QuerydslOrderProvider; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.bid.dto.query.BiddingRecord; +import org.chzz.market.domain.bid.dto.query.QBiddingRecord; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.bid.dto.response.QBidInfoResponse; +import org.chzz.market.domain.bid.entity.Bid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; @@ -51,4 +61,63 @@ public Page findBidsByAuctionId(Long auctionId, Pageable pageab select(bid.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } + + /** + * 나의 입찰 목록 조회 + */ + public Page findUsersBidHistory(Long userId, Pageable pageable, AuctionStatus auctionStatus) { + // 공통된 부분을 baseQuery로 추출 + JPAQuery baseQuery = jpaQueryFactory + .from(bid) + .join(auctionV2).on(bid.auctionId.eq(auctionV2.id) + .and(bid.bidderId.eq(userId)) + .and(bid.status.eq(ACTIVE)) + .and(auctionStatusEqIgnoreNull(auctionStatus))); + + List result = baseQuery + .select(new QBiddingRecord( + auctionV2.id, + auctionV2.name, + auctionV2.minPrice.longValue(), + bid.amount, + auctionV2.bidCount, + imageV2.cdnPath, + timeRemaining().longValue() + )) + .leftJoin(imageV2).on(imageV2.auction.eq(auctionV2).and(isRepresentativeImage())) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 카운트 쿼리 작성 + JPAQuery countQuery = baseQuery + .select(bid.count()); + + return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); + } + + /** + * 특정 경매의 모든 입찰 Entity 조회 + */ + public List findAllBidsByAuction(AuctionV2 auction) { + return jpaQueryFactory + .selectFrom(bid) + .where(bid.auctionId.eq(auction.getId()).and(bid.status.eq(ACTIVE))) + .orderBy(bid.amount.desc(), bid.updatedAt.asc()) + .fetch(); + } + + private BooleanExpression isRepresentativeImage() { + return imageV2.auction.eq(auctionV2).and(imageV2.sequence.eq(1)); + } + + private static NumberExpression timeRemaining() { + return numberTemplate(Integer.class, + "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 + } + + private BooleanBuilder auctionStatusEqIgnoreNull(AuctionStatus status) { + return nullSafeBuilderIgnore(() -> auctionV2.status.eq(status)); + } } diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java b/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java new file mode 100644 index 00000000..def45325 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java @@ -0,0 +1,34 @@ +package org.chzz.market.domain.bid.service; + +import static org.chzz.market.domain.bid.error.BidErrorCode.BID_NOT_FOUND; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.common.aop.redisrock.DistributedLock; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.error.BidException; +import org.chzz.market.domain.bid.repository.BidRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BidCancelLockService { + + private final AuctionV2Repository auctionRepository; + private final BidRepository bidRepository; + + /** + * 분산 락을 사용한 입찰 취소 + */ + @Transactional + @DistributedLock(key = "'bid:' + #userId + ':' + #auctionId") + public void cancel(Long auctionId, Long bidId, Long userId) { + Bid bid = bidRepository.findById(bidId).orElseThrow(() -> new BidException(BID_NOT_FOUND)); + bid.cancelBid(); + auctionRepository.decrementBidCount(auctionId); + log.info("입찰이 취소되었습니다. 입찰 ID: {}, 사용자 ID: {}, 경매 ID: {}", bid.getId(), userId, auctionId); + } +} diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidCancelService.java b/src/main/java/org/chzz/market/domain/bid/service/BidCancelService.java new file mode 100644 index 00000000..393db185 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/bid/service/BidCancelService.java @@ -0,0 +1,30 @@ +package org.chzz.market.domain.bid.service; + +import static org.chzz.market.domain.bid.error.BidErrorCode.BID_NOT_ACCESSIBLE; +import static org.chzz.market.domain.bid.error.BidErrorCode.BID_NOT_FOUND; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.error.BidException; +import org.chzz.market.domain.bid.repository.BidRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BidCancelService { + private final BidRepository bidRepository; + private final BidCancelLockService bidCancelLockService; + + /** + * 입찰 취소 + */ + public void cancel(Long bidId, Long userId) { + Bid bid = bidRepository.findById(bidId).orElseThrow(() -> new BidException(BID_NOT_FOUND)); + if (!bid.isOwner(userId)) { + throw new BidException(BID_NOT_ACCESSIBLE); + } + bidCancelLockService.cancel(bid.getAuctionId(), bidId, userId); + } +} diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java b/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java new file mode 100644 index 00000000..17b31fb7 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java @@ -0,0 +1,65 @@ +package org.chzz.market.domain.bid.service; + +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BELOW_MIN_PRICE; +import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BY_OWNER; +import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.common.aop.redisrock.DistributedLock; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.dto.BidCreateRequest; +import org.chzz.market.domain.bid.error.BidException; +import org.chzz.market.domain.bid.repository.BidRepository; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.error.exception.UserException; +import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class BidCreateService { + private final AuctionV2Repository auctionRepository; + private final BidRepository bidRepository; + private final UserRepository userRepository; + + @Transactional + @DistributedLock(key = "'bid:' + #userId + ':' + #bidCreateRequest.auctionId") + public void create(final BidCreateRequest bidCreateRequest, Long userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new UserException(USER_NOT_FOUND)); + AuctionV2 auction = auctionRepository.findById(bidCreateRequest.getAuctionId()) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + validateBidConditions(bidCreateRequest, user.getId(), auction); + bidRepository.findByAuctionIdAndBidderId(auction.getId(), userId) + .ifPresentOrElse( + // 이미 입찰을 한 경우 + bid -> bid.adjustBidAmount(bidCreateRequest.getBidAmount()), + // 입찰을 처음 하는 경우 + () -> { + bidRepository.save(bidCreateRequest.toEntity(user.getId())); + auctionRepository.incrementBidCount(auction.getId()); + } + ); + } + + /** + * 입찰 상태 유효성 검사 + */ + private void validateBidConditions(BidCreateRequest bidCreateRequest, Long userId, AuctionV2 auction) { + // 경매 등록자가 입찰할 때 + if (auction.isOwner(userId)) { + throw new BidException(BID_BY_OWNER); + } + auction.validateAuctionEndTime(); + // 최소 금액보다 낮은 금액일 때 + if (!auction.isAboveMinPrice(bidCreateRequest.getBidAmount())) { + throw new BidException(BID_BELOW_MIN_PRICE); + } + } +} diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java b/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java index 313cee69..b9218eb7 100644 --- a/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java +++ b/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java @@ -3,11 +3,15 @@ import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.AuctionV2; import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.dto.query.BiddingRecord; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.repository.BidQueryRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -21,6 +25,9 @@ public class BidLookupService { private final AuctionV2Repository auctionRepository; private final BidQueryRepository bidQueryRepository; + /** + * 특정 경매의 모든 입찰 조회 + */ public Page getBidsByAuctionId(Long userId, Long auctionId, Pageable pageable) { AuctionV2 auction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); @@ -30,4 +37,18 @@ public Page getBidsByAuctionId(Long userId, Long auctionId, Pag auction.validateAuctionEnded(); return bidQueryRepository.findBidsByAuctionId(auctionId, pageable); } + + /** + * 나의 입찰 목록 조회 + */ + public Page inquireBidHistory(Long userId, Pageable pageable, AuctionStatus status) { + return bidQueryRepository.findUsersBidHistory(userId, pageable, status); + } + + /** + * 특정 경매의 입찰 Entity 조회 (경매 종료스케줄링에 사용) + */ + public List findAllBidsByAuction(AuctionV2 auction) { + return bidQueryRepository.findAllBidsByAuction(auction); + } } diff --git a/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java b/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java index a15a2045..8af9ea55 100644 --- a/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java +++ b/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java @@ -1,9 +1,12 @@ package org.chzz.market.domain.likev2.repository; import java.util.List; +import java.util.Optional; import org.chzz.market.domain.likev2.entity.LikeV2; import org.springframework.data.jpa.repository.JpaRepository; public interface LikeV2Repository extends JpaRepository { List findByAuctionId(Long auctionId); + + Optional findByUserIdAndAuctionId(Long userId, Long auctionId); } diff --git a/src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java b/src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java new file mode 100644 index 00000000..50bef41d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java @@ -0,0 +1,54 @@ +package org.chzz.market.domain.likev2.service; + +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.aop.redisrock.DistributedLock; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.likev2.entity.LikeV2; +import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class LikeUpdateService { + private final AuctionV2Repository auctionRepository; + private final LikeV2Repository likeRepository; + + @DistributedLock(key = "'like:' + #userId + ':' + #auctionId") + public void updateLike(Long userId, Long auctionId) { + // 락 획득 후 트랜잭션 시작 + handleLikeTransaction(userId, auctionId); + } + + @Transactional + public void handleLikeTransaction(Long userId, Long auctionId) { + auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + + likeRepository.findByUserIdAndAuctionId(userId, auctionId) + .ifPresentOrElse( + like -> handleUnlike(like, auctionId), + () -> handleLike(userId, auctionId) + ); + } + + private void handleUnlike(LikeV2 like, Long auctionId) { + likeRepository.delete(like); + auctionRepository.decrementLikeCount(auctionId); + } + + private void handleLike(Long userId, Long auctionId) { + likeRepository.save(createLike(userId, auctionId)); + auctionRepository.incrementLikeCount(auctionId); + } + + private LikeV2 createLike(Long userId, Long auctionId) { + return LikeV2.builder() + .userId(userId) + .auctionId(auctionId) + .build(); + } +} diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java new file mode 100644 index 00000000..488231e5 --- /dev/null +++ b/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java @@ -0,0 +1,162 @@ +package org.chzz.market.domain.bid.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.error.BidErrorCode; +import org.chzz.market.domain.bid.error.BidException; +import org.chzz.market.domain.bid.repository.BidRepository; +import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BidCancelLockServiceTest { + + @Autowired + private BidCancelLockService bidCancelLockService; + + @Autowired + private AuctionV2Repository auctionRepository; + + @Autowired + private BidRepository bidRepository; + + @Autowired + private UserRepository userRepository; + + private AuctionV2 auction; + private User seller; + private List users; + private List bids; + private ImageV2 defaultImage; + + @BeforeEach + public void setUp() { + seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); + userRepository.save(seller); + defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + users = IntStream.range(1, 6) + .mapToObj(i -> User.builder() + .email("user" + i + "@example.com") + .providerId("user" + i) + .providerType(User.ProviderType.KAKAO) + .build()) + .map(userRepository::save) + .collect(Collectors.toList()); + auction = auctionRepository.save( + createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null)); + users.forEach(user -> bidRepository.save( + Bid.builder().auctionId(auction.getId()).bidderId(user.getId()).amount(1000L).build())); + bids = users.stream() + .map(user -> Bid.builder() + .auctionId(auction.getId()) + .bidderId(user.getId()) + .amount(1000L) + .build()) + .map(bidRepository::save) + .collect(Collectors.toList()); + } + + @Test + public void multipleUsersCancelBidTest() throws InterruptedException { + int numberOfThreads = users.size(); + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + for (int i = 0; i < numberOfThreads; i++) { + final long userId = users.get(i).getId(); + final long bidId = bids.get(i).getId(); + executorService.execute(() -> { + try { + bidCancelLockService.cancel(auction.getId(), bidId, userId); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + // Auction 업데이트 후 결과 검증 + AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + long bidCount = updatedAuction.getBidCount(); + + // 모든 입찰 취소 후 카운트 0 검증 + assertThat(bidCount).isEqualTo(0); + } + + @Test + public void singleUserConcurrentCancelBidTest_ThrowsException() throws InterruptedException { + int numberOfThreads = 3; // 동일한 사용자가 동시에 요청 + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + // 예외를 수집할 리스트 + List exceptions = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 0; i < numberOfThreads; i++) { + final long userId = users.get(0).getId(); + final long bidId = bids.get(0).getId(); + executorService.execute(() -> { + try { + bidCancelLockService.cancel(auction.getId(), bidId, userId); + } catch (Exception e) { + exceptions.add(e); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + // 하나의 성공한 요청과 두 개의 예외 발생 확인 + assertThat(exceptions).hasSize(numberOfThreads - 1); // 예외는 2개 발생해야 함 + assertThat(exceptions.get(0)) + .isInstanceOf(BidException.class) + .extracting("errorCode") + .isEqualTo(BidErrorCode.BID_ALREADY_CANCELLED); + + // 최종 입찰 수 확인 (4가 되어야 함) + AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + long bidCount = updatedAuction.getBidCount(); + assertThat(bidCount).isEqualTo(4); + } + + private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + AuctionV2 auction = AuctionV2.builder() + .seller(seller) + .name(name) + .description(description) + .status(status) + .category(Category.ELECTRONICS) + .winnerId(winnerId) + .minPrice(1000) + .bidCount(5L) + .endDateTime(LocalDateTime.now().plusDays(2)) + .build(); + auction.addImage(defaultImage); + auctionRepository.save(auction); + return auction; + } +} diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java new file mode 100644 index 00000000..bb43ad2f --- /dev/null +++ b/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java @@ -0,0 +1,142 @@ +package org.chzz.market.domain.bid.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.dto.BidCreateRequest; +import org.chzz.market.domain.bid.error.BidErrorCode; +import org.chzz.market.domain.bid.error.BidException; +import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class BidCreateServiceConcurrencyTest { + + @Autowired + private BidCreateService bidCreateService; + + @Autowired + private AuctionV2Repository auctionRepository; + + @Autowired + private UserRepository userRepository; + + private AuctionV2 auction; + private User seller; + private List users; + private ImageV2 defaultImage; + + @BeforeEach + public void setUp() { + seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); + userRepository.save(seller); + defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + auction = auctionRepository.save( + createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null)); + users = IntStream.range(1, 6) + .mapToObj(i -> User.builder() + .email("user" + i + "@example.com") + .providerId("user" + i) + .providerType(User.ProviderType.KAKAO) + .build()) + .map(userRepository::save) + .collect(Collectors.toList()); + } + + @Test + public void 하나의_경매에_여러명이_입찰할때_동시성테스트() throws InterruptedException { + int numberOfThreads = 5; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + for (int i = 0; i < numberOfThreads; i++) { + final long userId = users.get(i).getId(); + executorService.execute(() -> { + try { + BidCreateRequest bidRequest = new BidCreateRequest(auction.getId(), 1000L); + bidCreateService.create(bidRequest, userId); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + long bidCount = updatedAuction.getBidCount(); + assertThat(bidCount).isEqualTo(numberOfThreads); + } + + @Test + public void 하나의경매에_동일한_사용자가_입찰요청을_할경우_예외가_발생한다() throws InterruptedException { + int numberOfThreads = 3; // 동일한 사용자가 동시에 요청 + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + // 예외를 수집할 리스트 + List exceptions = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 0; i < numberOfThreads; i++) { + executorService.execute(() -> { + try { + BidCreateRequest bidRequest = new BidCreateRequest(auction.getId(), 1000L); + bidCreateService.create(bidRequest, users.get(0).getId()); + } catch (Exception e) { + exceptions.add(e); // 예외를 리스트에 추가 + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + // 하나의 성공한 입찰과 두 개의 예외 발생 확인 + assertThat(exceptions).hasSize(numberOfThreads - 1); // 예외는 두 개 발생해야 함 + assertThat(exceptions.get(0)) + .isInstanceOf(BidException.class) + .extracting("errorCode") + .isEqualTo(BidErrorCode.BID_SAME_AS_PREVIOUS); + + // 최종 입찰 수 확인 (1번만 성공) + AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + long bidCount = updatedAuction.getBidCount(); + assertThat(bidCount).isEqualTo(1); + } + + private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + AuctionV2 auction = AuctionV2.builder() + .seller(seller) + .name(name) + .description(description) + .status(status) + .category(Category.ELECTRONICS) + .winnerId(winnerId) + .minPrice(1000) + .endDateTime(LocalDateTime.now().plusDays(2)) + .build(); + auction.addImage(defaultImage); + auctionRepository.save(auction); + return auction; + } +} diff --git a/src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java b/src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java new file mode 100644 index 00000000..7f6a14c8 --- /dev/null +++ b/src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java @@ -0,0 +1,119 @@ +package org.chzz.market.domain.likev2.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class LikeUpdateServiceConcurrencyTest { + @Autowired + private UserRepository userRepository; + + @Autowired + private LikeUpdateService likeUpdateService; + + @Autowired + private AuctionV2Repository auctionRepository; + + private User seller; + private User user; + private ImageV2 defaultImage; + + @BeforeEach + void setUp() { + seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); + user = User.builder().email("user").providerId("user").providerType(User.ProviderType.KAKAO).build(); + defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + userRepository.save(seller); + userRepository.save(user); + } + + @Test + public void 좋아요_동시성_테스트() throws InterruptedException { + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + + int numberOfThreads = 10; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + // WHEN + for (int i = 0; i < numberOfThreads; i++) { + long userId = i + 1; + executorService.execute(() -> { + try { + likeUpdateService.updateLike(userId, auction.getId()); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()) + .orElseThrow(() -> new RuntimeException("Auction not found")); + assertThat(updatedAuction.getLikeCount()).isEqualTo(10); + } + + @Test + public void 한사람_동시에_여러_좋아요_요청_테스트() throws InterruptedException { + // 경매 생성 + AuctionV2 auction = createAuction(seller, "아이폰 13", "최신형 아이폰 13 팝니다.", AuctionStatus.PROCEEDING, null); + + int numberOfThreads = 9; // 동시에 요청할 스레드 수 + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + // 하나의 userId를 여러 번 요청 + long userId = user.getId(); + + // WHEN + for (int i = 0; i < numberOfThreads; i++) { + executorService.execute(() -> { + try { + likeUpdateService.updateLike(userId, auction.getId()); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + // THEN + AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()) + .orElseThrow(() -> new RuntimeException("Auction not found")); + + assertThat(updatedAuction.getLikeCount()).isEqualTo(1); + } + + + private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + AuctionV2 auction = AuctionV2.builder() + .seller(seller) + .name(name) + .description(description) + .status(status) + .category(Category.ELECTRONICS) + .winnerId(winnerId) + .build(); + auction.addImage(defaultImage); + auctionRepository.save(auction); + return auction; + } +} From eafccc9a7202ff33bc494734eba4dbd539b6ce2b Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:37:48 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * delete: 사용하지 않는 파일 제거 * refactor: API 재구성 * feat: BaseAuctionResponse 추가 * feat: OfficialAuctionResponse 정식 경매 응답 DTO 추가 * feat: PreAuctionResponse 사전 경매 응답 DTO 추가 * feat: 경매목록 조회 서비스 함수 추가 * feat: 경매목록 조회 쿼리 추가 * chore: 경매목록 조회 security 설정 해제 * feat: 경매목록 조회 API 함수 전환 * refactor: 경매 상태 파라미터 default value 설정 * docs: 경매 목록 조회 API 문서 추가 * test: 경매 목록 조회 테스트 추가 * refactor: 이미지 조인 방식 수정 --- .../market/common/config/SecurityConfig.java | 1 + .../auctionv2/controller/AuctionV2Api.java | 66 ++++++- .../controller/AuctionV2Controller.java | 87 ++++++++- .../dto/response/BaseAuctionResponse.java | 22 +++ .../dto/response/OfficialAuctionResponse.java | 20 +++ .../dto/response/PreAuctionResponse.java | 18 ++ .../auctionv2/dto/view/AuctionType.java | 8 - .../auctionv2/dto/view/UserAuctionType.java | 8 - .../repository/AuctionV2QueryRepository.java | 133 ++++++++++++-- .../service/AuctionLookupService.java | 27 +++ .../AuctionV2QueryRepositoryTest.java | 170 +++++++++++++++++- 11 files changed, 503 insertions(+), 57 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/view/AuctionType.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/view/UserAuctionType.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java diff --git a/src/main/java/org/chzz/market/common/config/SecurityConfig.java b/src/main/java/org/chzz/market/common/config/SecurityConfig.java index 6b1ba1df..c6e0e8b9 100644 --- a/src/main/java/org/chzz/market/common/config/SecurityConfig.java +++ b/src/main/java/org/chzz/market/common/config/SecurityConfig.java @@ -72,6 +72,7 @@ public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception "/api/v1/users/*", "/api/v1/users/check/nickname/*").permitAll() .requestMatchers(GET, + "/api/v2/auctions", "/api/v2/auctions/categories", "/api/v2/auctions/{auctionId:\\d+}").permitAll() .requestMatchers(POST, diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java index f16548c3..98bdf8c3 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java @@ -1,6 +1,11 @@ package org.chzz.market.domain.auctionv2.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; @@ -8,8 +13,9 @@ import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; import org.chzz.market.domain.auction.dto.response.RegisterResponse; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; -import org.chzz.market.domain.auctionv2.dto.view.AuctionType; -import org.chzz.market.domain.auctionv2.dto.view.UserAuctionType; +import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; @@ -26,20 +32,64 @@ @Tag(name = "auctions(v2)", description = "V2 경매 API") @RequestMapping("/v2/auctions") public interface AuctionV2Api { - @Operation(summary = "경매 목록 조회", description = "경매 목록을 조회합니다. type 파라미터를 통해 조회 유형을 지정합니다.") + @Operation(summary = "경매 목록 조회", description = "경매 목록을 조회합니다. status 파라미터를 통해 조회 유형을 지정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정식 경매 응답(페이징)", + content = {@Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = OfficialAuctionResponse.class)) + )} + ), + @ApiResponse(responseCode = "201", description = "사전 경매 응답(페이징)", + content = {@Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = PreAuctionResponse.class)) + )} + ) + }) @GetMapping ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(required = false) Category category, - @RequestParam AuctionType type, + @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); @Operation(summary = "경매 카테고리 조회", description = "경매 카테고리 목록을 조회합니다.") @GetMapping("/categories") ResponseEntity> getCategoryList(); - @Operation(summary = "사용자 경매 목록 조회", description = "사용자가 등록한 경매 목록을 조회합니다. type 파라미터를 통해 조회 유형을 지정합니다.") - @GetMapping("/users") - ResponseEntity> getUserAuctionList(@LoginUser Long userId, @RequestParam UserAuctionType type, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + @Operation(summary = "마감임박 조회", description = "정식 경매의 마감임박") + @GetMapping("/imminent") + ResponseEntity> getImminentAuctionList( + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 등록한 진행중인 경매 목록 조회", description = "사용자가 등록한 진행중인 경매 목록을 조회합니다.") + @GetMapping("/users/proceeding") + ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 등록한 종료된 경매 목록 조회", description = "사용자가 등록한 종료된 경매 목록을 조회합니다.") + @GetMapping("/users/ended") + ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 등록한 사전 경매 목록 조회", description = "사용자가 등록한 사전 경매 목록을 조회합니다.") + @GetMapping("/users/pre") + ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 낙찰한 경매 목록 조회", description = "사용자가 낙찰한 경매 목록을 조회합니다.") + @GetMapping("/users/won") + ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 낙찰실패한 경매 목록 조회", description = "사용자가 낙찰실패한 경매 목록을 조회합니다.") + @GetMapping("/users/lost") + ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 좋아요(찜)한 경매 목록 조회", description = "사용자가 좋아요(찜)한 경매 목록을 조회합니다.") + @GetMapping("/users/likes") + ResponseEntity> getUserLikesAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); @Operation(summary = "경매 등록", description = "경매를 등록합니다.") @PostMapping diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java index 43fed9d2..f3f8acd5 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java @@ -7,10 +7,10 @@ import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; import org.chzz.market.domain.auction.dto.response.RegisterResponse; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; -import org.chzz.market.domain.auctionv2.dto.view.AuctionType; -import org.chzz.market.domain.auctionv2.dto.view.UserAuctionType; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.auctionv2.service.AuctionCategoryService; +import org.chzz.market.domain.auctionv2.service.AuctionLookupService; import org.chzz.market.domain.auctionv2.service.AuctionTestService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -27,32 +27,98 @@ @RestController @RequiredArgsConstructor public class AuctionV2Controller implements AuctionV2Api { + private final AuctionLookupService auctionLookupService; private final AuctionCategoryService auctionCategoryService; private final AuctionTestService testService; + /** + * 경매 목록 조회 + */ @Override @GetMapping public ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(required = false) Category category, - @RequestParam AuctionType type, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionLookupService.getAuctionList(userId, category, status, pageable)); } + /** + * 경매 카테고리 Enum 조회 + */ @Override @GetMapping("/categories") public ResponseEntity> getCategoryList() { return ResponseEntity.ok(auctionCategoryService.getCategories()); } + /** + * 정식 경매의 마감임박 조회 + */ + @Override + @GetMapping("/imminent") + public ResponseEntity> getImminentAuctionList(@PageableDefault(sort = "newest") Pageable pageable) { + return null; + } + + /** + * 사용자가 등록한 진행중인 경매 목록 조회 + */ + @Override + @GetMapping("/users/proceeding") + public ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return null; + } + + /** + * 사용자가 등록한 종료된 경매 목록 조회 + */ + @Override + public ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return null; + } + + /** + * 사용자가 등록한 사전 경매 목록 조회 + */ + @Override + public ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return null; + } + + /** + * 사용자가 낙찰한 경매 목록 조회 + */ + @Override + public ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return null; + } + + /** + * 사용자가 낙찰실패한 경매 목록 조회 + */ + @Override + public ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return null; + } + + /** + * 사용자가 좋아요(찜)한 경매 목록 조회 + */ @Override - @GetMapping("/users") - public ResponseEntity> getUserAuctionList(@LoginUser Long userId, - @RequestParam UserAuctionType type, - @PageableDefault(sort = "newest") Pageable pageable) { + public ResponseEntity> getUserLikesAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { return null; } + /** + * 경매 등록 + */ @Override @PostMapping public ResponseEntity registerAuction(@LoginUser Long userId, @@ -61,6 +127,9 @@ public ResponseEntity registerAuction(@LoginUser Long userId, return null; } + /** + * 경매 테스트 등록 + */ @Override @PostMapping("/test") public ResponseEntity testEndAuction(@LoginUser Long userId, diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java new file mode 100644 index 00000000..b883a28d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java @@ -0,0 +1,22 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public abstract class BaseAuctionResponse { + private Long auctionId; + private String productName; + private String imageUrl; + private Long minPrice; + private Boolean isSeller; + + public BaseAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller) { + this.auctionId = auctionId; + this.productName = productName; + this.imageUrl = imageUrl; + this.minPrice = minPrice; + this.isSeller = isSeller; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java new file mode 100644 index 00000000..2285110a --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java @@ -0,0 +1,20 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OfficialAuctionResponse extends BaseAuctionResponse { + private Long timeRemaining; + private Long participantCount; + private Boolean isParticipated; + + public OfficialAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long timeRemaining, Long participantCount, Boolean isParticipated) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.timeRemaining = timeRemaining; + this.participantCount = participantCount; + this.isParticipated = isParticipated; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java new file mode 100644 index 00000000..eaeed98a --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java @@ -0,0 +1,18 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PreAuctionResponse extends BaseAuctionResponse { + private Long likeCount; + private Boolean isLiked; + + public PreAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long likeCount, Boolean isLiked) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.likeCount = likeCount; + this.isLiked = isLiked; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/view/AuctionType.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/view/AuctionType.java deleted file mode 100644 index 47dc42fa..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/view/AuctionType.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto.view; - -import lombok.Getter; - -@Getter -public enum AuctionType { - OFFICIAL, PRE, BEST, IMMINENT -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/view/UserAuctionType.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/view/UserAuctionType.java deleted file mode 100644 index 4902e046..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/view/UserAuctionType.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto.view; - -import lombok.Getter; - -@Getter -public enum UserAuctionType { - PROCEEDING, ENDED, PRE, WON, LOST, LIKED; -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java index 007bad49..0c6e0fc4 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java @@ -2,6 +2,9 @@ import static com.querydsl.core.types.dsl.Expressions.numberTemplate; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; +import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PRE; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PROCEEDING; import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; @@ -12,27 +15,40 @@ import static org.chzz.market.domain.user.entity.QUser.user; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; -import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.NumberExpression; -import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Optional; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.chzz.market.common.util.QuerydslOrder; +import org.chzz.market.common.util.QuerydslOrderProvider; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.QWonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.bid.entity.QBid; import org.chzz.market.domain.image.dto.ImageResponse; import org.chzz.market.domain.image.dto.QImageResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; @Repository @RequiredArgsConstructor public class AuctionV2QueryRepository { private final JPAQueryFactory jpaQueryFactory; + private final QuerydslOrderProvider querydslOrderProvider; /** * 낙찰자 정보 조회 @@ -43,11 +59,14 @@ public Optional findWinningBidById(Long auctionId) { .from(auctionV2) .leftJoin(bid).on(bid.bidderId.eq(auctionV2.winnerId) .and(bid.auctionId.eq(auctionV2.id))) - .leftJoin(imageV2).on(isRepresentativeImage()) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) .where(auctionV2.id.eq(auctionId)) .fetchOne()); } + /** + * 사전 경매 상세 조회 + */ public Optional findPreAuctionDetailById(Long userId, Long auctionId) { Optional result = Optional.ofNullable(jpaQueryFactory .select( @@ -64,11 +83,12 @@ public Optional findPreAuctionDetailById(Long userId, auctionV2.category, auctionV2.updatedAt, auctionV2.likeCount, - isAuctionLikedByUserId(userId) + likeV2.id.isNotNull() ) ) .from(auctionV2) .join(auctionV2.seller, user) + .leftJoin(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeUserIdEq(userId))) .where(auctionV2.id.eq(auctionId)) .fetchOne()); @@ -76,6 +96,9 @@ public Optional findPreAuctionDetailById(Long userId, return result; } + /** + * 정식 경매 상세 조회 + */ public Optional findOfficialAuctionDetailById(Long userId, Long auctionId) { QBid activeBid = new QBid("bidActive"); QBid canceledBid = new QBid("bidCanceled"); @@ -122,6 +145,76 @@ public Optional findOfficialAuctionDetailById(Lon return officialAuctionDetailResponse; } + /** + * 사전 경매 목록 조회 + */ + public Page findPreAuctions(Long userId, Category category, Pageable pageable) { + List content = jpaQueryFactory.from(auctionV2) + .select( + Projections.constructor( + PreAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + auctionV2.likeCount, + likeV2.id.isNotNull() + ) + ) + .from(auctionV2) + .join(auctionV2.seller, user) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .leftJoin(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeUserIdEq(userId))) + .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PRE))) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = jpaQueryFactory.select(auctionV2.count()) + .from(auctionV2) + .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PRE))); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + /** + * 정식 경매 목록 조회 + */ + public Page findOfficialAuctions(Long userId, Category category, AuctionStatus status, + Pageable pageable) { + List content = jpaQueryFactory.from(auctionV2) + .select( + Projections.constructor( + OfficialAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + timeRemaining().longValue(), + auctionV2.bidCount, + bid.id.isNotNull() + ) + ) + .from(auctionV2) + .join(auctionV2.seller, user) + .leftJoin(bid).on(bid.auctionId.eq(auctionV2.id).and(bidderIdEq(userId)).and(bid.status.eq(ACTIVE))) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(status))) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = jpaQueryFactory.select(auctionV2.count()) + .from(auctionV2) + .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PROCEEDING))); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + private List getImagesByAuctionId(Long auctionId) { return jpaQueryFactory .select(new QImageResponse(imageV2.id, imageV2.cdnPath)) @@ -131,22 +224,14 @@ private List getImagesByAuctionId(Long auctionId) { .fetch(); } - private BooleanExpression isAuctionLikedByUserId(Long userId) { - return JPAExpressions.selectOne() - .from(likeV2) - .where(likeV2.auctionId.eq(auctionV2.id) - .and(likeUserIdEq(userId))) - .exists(); - } - - private BooleanExpression isRepresentativeImage() { - return imageV2.auction.eq(auctionV2).and(imageV2.sequence.eq(1)); - } - private BooleanBuilder userIdEq(Long userId) { return nullSafeBuilder(() -> user.id.eq(userId)); } + private BooleanBuilder bidderIdEq(Long userId) { + return nullSafeBuilder(() -> bid.bidderId.eq(userId)); + } + private BooleanBuilder bidderIdEqSub(QBid qBid, Long userId) { return nullSafeBuilder(() -> qBid.bidderId.eq(userId)); } @@ -159,8 +244,24 @@ private BooleanBuilder likeUserIdEq(Long userId) { return nullSafeBuilder(() -> likeV2.userId.eq(userId)); } + private BooleanBuilder categoryEqIgnoreNull(Category category) { + return nullSafeBuilderIgnore(() -> auctionV2.category.eq(category)); + } + private static NumberExpression timeRemaining() { return numberTemplate(Integer.class, "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public enum AuctionOrder implements QuerydslOrder { + POPULARITY("popularity-v2", auctionV2.bidCount.desc()), + EXPENSIVE("expensive-v2", auctionV2.minPrice.desc()), + CHEAP("cheap-v2", auctionV2.minPrice.asc()), + NEWEST("newest-v2", auctionV2.createdAt.desc()); + + private final String name; + private final OrderSpecifier orderSpecifier; + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java new file mode 100644 index 00000000..a836c7f7 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java @@ -0,0 +1,27 @@ +package org.chzz.market.domain.auctionv2.service; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AuctionLookupService { + private final AuctionV2QueryRepository auctionQueryRepository; + + /** + * 경매 목록 조회 + */ + public Page getAuctionList(Long userId, Category category, AuctionStatus status, Pageable pageable) { + return switch (status) { + case PRE -> auctionQueryRepository.findPreAuctions(userId, category, pageable); + case PROCEEDING, ENDED -> auctionQueryRepository.findOfficialAuctions(userId, category, status, pageable); + }; + } +} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java index 32cc9bf2..ee15a7a0 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java @@ -4,13 +4,18 @@ import java.util.Optional; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.AuctionV2; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.entity.Bid.BidStatus; import org.chzz.market.domain.bid.repository.BidRepository; import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.likev2.entity.LikeV2; +import org.chzz.market.domain.likev2.repository.LikeV2Repository; import org.chzz.market.domain.orderv2.entity.OrderV2; import org.chzz.market.domain.orderv2.repository.OrderV2Repository; import org.chzz.market.domain.paymentv2.entity.PaymentV2.PaymentMethod; @@ -22,6 +27,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; @SpringBootTest @@ -37,6 +46,8 @@ class AuctionV2QueryRepositoryTest { private UserRepository userRepository; @Autowired private OrderV2Repository orderRepository; + @Autowired + private LikeV2Repository likeV2Repository; private User seller; private User user; @@ -52,7 +63,8 @@ void setUp() { userRepository.save(user); } - private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId, + Integer minPrice) { AuctionV2 auction = AuctionV2.builder() .seller(seller) .name(name) @@ -60,6 +72,7 @@ private AuctionV2 createAuction(User seller, String name, String description, Au .status(status) .category(Category.ELECTRONICS) .winnerId(winnerId) + .minPrice(minPrice) .build(); auction.addImage(defaultImage); auctionV2Repository.save(auction); @@ -99,7 +112,8 @@ private OrderV2 createOrder(AuctionV2 auction, User buyer, Long amount) { @Test void 낙찰정보를_조회한다() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, user.getId()); + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, user.getId(), + 1000); createBid(user, auction, 2000L, Bid.BidStatus.ACTIVE); // When @@ -116,7 +130,8 @@ class OfficialAuctionDetail { @Test void 본인의_제품을_조회한경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, seller.getId()); + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, seller.getId(), + 1000); // When Optional result = auctionQueryRepository.findOfficialAuctionDetailById( @@ -134,7 +149,7 @@ class OfficialAuctionDetail { @Test void 다른_사람_경매를_참여안한경우_조회한경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); // When Optional result = auctionQueryRepository.findOfficialAuctionDetailById( @@ -150,7 +165,7 @@ class OfficialAuctionDetail { @Test void 다른_사람_경매을_참여한경우_조회() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); createBid(user, auction, 1000L, Bid.BidStatus.ACTIVE); // When @@ -177,7 +192,7 @@ class OfficialAuctionDetail { @Test void 비로그인_상태에서_조회한_경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); // When Optional result = auctionQueryRepository.findOfficialAuctionDetailById( @@ -196,7 +211,7 @@ class OfficialAuctionDetail { @Test void 취소된_입찰이_있는_경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); createBid(user, auction, 2000L, Bid.BidStatus.CANCELLED); // When @@ -213,7 +228,7 @@ class OfficialAuctionDetail { void 주문이_있을시_조회를_한다() { // Given AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, - user.getId()); + user.getId(), 2000); createBid(user, auction, 2000L, Bid.BidStatus.ACTIVE); createOrder(auction, user, 2000L); @@ -231,4 +246,143 @@ class OfficialAuctionDetail { assertThat(response.getIsOrdered()).isTrue(); } } + + @Nested + @DisplayName("경매 목록 조회") + class Auctions { + @Test + public void 정식경매_목록_조회_테스트_본인것() throws Exception { + //given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + auctionV2Repository.save(auction); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + //when + Page result = auctionQueryRepository.findOfficialAuctions(seller.getId(), + Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + //then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getIsSeller()).isTrue(); + } + + @Test + public void 정식경매_목록_조회_테스트_남의것() throws Exception { + //given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + auctionV2Repository.save(auction); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + //when + Page result = auctionQueryRepository.findOfficialAuctions(user.getId(), + Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + //then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getIsSeller()).isFalse(); + assertThat(result.getContent().get(0).getIsParticipated()).isFalse(); + + //when 비로그인 + Page result1 = auctionQueryRepository.findOfficialAuctions(null, + Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + //then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getIsSeller()).isFalse(); + assertThat(result.getContent().get(0).getIsParticipated()).isFalse(); + } + + @Test + public void 정식경매_목록_조회_테스트_입찰을했을때() throws Exception { + //given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + auctionV2Repository.save(auction); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Bid bid = createBid(user, auction, 1000L, BidStatus.ACTIVE); + bidRepository.save(bid); + //when + Page result = auctionQueryRepository.findOfficialAuctions(user.getId(), + Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + //then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getIsSeller()).isFalse(); + assertThat(result.getContent().get(0).getIsParticipated()).isTrue(); + } + + @Test + public void 사전경매_목록조회_좋아요를_했을때() { + //given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + auctionV2Repository.save(auction); + LikeV2 like = LikeV2.builder().auctionId(auction.getId()).userId(user.getId()).build(); + likeV2Repository.save(like); + + //when + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Page result = auctionQueryRepository.findPreAuctions(user.getId(), + Category.ELECTRONICS, pageable); + //then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getIsSeller()).isFalse(); + assertThat(result.getContent().get(0).getIsLiked()).isTrue(); + } + + @Test + public void 사전경매_목록조회_좋아요를_안했을때() { + //given + AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + auctionV2Repository.save(auction); + + //when + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Page resultWithUserId = auctionQueryRepository.findPreAuctions(user.getId(), + Category.ELECTRONICS, pageable); + + assertThat(resultWithUserId).isNotNull(); + assertThat(resultWithUserId.getContent()).hasSize(1); + assertThat(resultWithUserId.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(resultWithUserId.getContent().get(0).getIsSeller()).isFalse(); + assertThat(resultWithUserId.getContent().get(0).getIsLiked()).isFalse(); + + // when - 비로그인 + Page resultWithNull = auctionQueryRepository.findPreAuctions(null, Category.ELECTRONICS, + pageable); + + assertThat(resultWithNull).isNotNull(); + assertThat(resultWithNull.getContent()).hasSize(1); + assertThat(resultWithNull.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(resultWithNull.getContent().get(0).getIsSeller()).isFalse(); + assertThat(resultWithNull.getContent().get(0).getIsLiked()).isFalse(); + } + + @Test + public void 정식경매_목록_조회_정렬_테스트() throws Exception { + //given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, + 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, + 2000); + auctionV2Repository.save(auction1); + auctionV2Repository.save(auction2); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + //when + Page result = auctionQueryRepository.findOfficialAuctions(seller.getId(), + Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + + //then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("아이패드"); // 가격이 더 높은 아이패드가 먼저 + assertThat(result.getContent().get(1).getProductName()).isEqualTo("맥북프로"); // 가격이 낮은 맥북프로가 나중 + } + } } From cd0803037605d74d97946c30284601637de011f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=98=88=EC=B0=AC?= <88381563+YeaChan05@users.noreply.github.com> Date: Sun, 24 Nov 2024 17:18:13 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20API=20=EC=A0=84=ED=99=98=20(#127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: context provider 구현 ApplicationContext을 정적으로 호출하기 위한 util 클래스 구현 * feat: 서비스 주입 가능한 요청 타입 구현 등록 서비스 선택 가능한 요청 타입 구현 * feat: 경매 등록 엔드포인트 구현 경매 등록 엔드포인트 구현 * feat: 정식 경매 등록 서비스 구현 정식 경매 등록 서비스 구현 * feat: 사전 경매 등록 서비스 구현 사전 경매 등록 서비스 구현 * feat: 이미지 업로드 이벤트 구현 이미지 업로드 이벤트 구현 * feat: 다수 이미지 업로드 서비스 구현 여러 이미지를 한번에 업로드하는 서비스 구현 * feat: 다수 파일 업로드 메서드 구현 여러개의 파일을 한번에 s3로 업로드하는 메서드 구현 * feat: ImageV2 저장소 구현 ImageV2 저장소 구현 * feat: 경매 등록 인터페이스 구현 경매 등록 서비스 인터페이스 구현 * feat: 경매 등록 요청 객체 구현 경매 등록 요청 객체 구현 * feat: 이미지 변환 에러코드 추가 이미지 변환 에러코드 추가 * move: 경로 수정 dto ->dto/request * fix: 불필요한 getter 제거 record에 불필요한 getter 어노테이션 제거 * refactor: dto to record dto를 record로 수정 * feat: 도메인 로직 구현 경매에 이미지 등록 로직 구현 * feat: 상품 등록 엔드포인트 구현 상품 등록 엔드포인트 구현 * feat: 이미지 맵 생성 로직 구현 이미지와 이름을 key-value 형태 구조로 관리 * fix: 다수 이미지 업로드 방식 수정 다수 이미지 업로드 방식을 기존 메서드 활용하는 방향으로 수정 * feat: 이미지 업로드 검증기 추가 MultipartFile 특성상 jakarta 검증방식 제한으로 인해 이미지가 없는 경우를 검증하기 위한 커스텀 어노테이션 구현 * feat: 이미지 업로드 검증기 적용 구헌된 검증기 엔드포인트에 적용 * test: 경매 등록 테스트코드 작성 통함 테스트를 통한 경매 등록 테스트 * docs: api 오류 수정 api 오류 중복 수정 * fix: 이미지 업로드 비동기로 전환 이미지 업로드 트랜잭션 이벤트를 동기로 변경 * fix: 이벤트 처리 시기 수정 이벤트 처리를 커밋 직전으로 변경 * fix: 경매 스케줄링 등록 구현 경매 등록 후 스케줄링 등록 기능 구현 * fix: 경매 도메인 로직 구현 경매 종료 처리 및 낙찰자 등록 로직 구현 * feat: 경매 종료 작업 구현 경매 종료시 동작할 작업 구현 * feat: 경매 종료 서비스 구현 경매 종료시 동작해야할 서비스 구현 * feat: 입찰 정렬 조회 구현 입찰 기록을 가격 내림차순으로 조회하도록 메서드 구현 * fix: bid 저장소 변경 입찰 정렬 조회 저장소 변경 * refactor: 경매 전환 이벤트 등록 경매 스케줄링 이벤트 등록 * feat: 경매 스케줄 객체 구현 경매 스케줄 객체 구현 * feat: 경매 스케줄러 구현 경매 스케줄러 구현 * fix: 경매 전환시 스케줄링 등록 기능 구현 경매 전환시 스케줄링 등록 기능 구현 * fix: 경매 종료시간 영속화 경매 전환시 종료 시간 등록 --- .../util/ApplicationContextProvider.java | 21 ++++ .../annotation/NotEmptyMultipartList.java | 34 ++++++ .../NotEmptyMultipartListValidator.java | 22 ++++ .../auctionv2/controller/AuctionV2Api.java | 30 ++++- .../controller/AuctionV2Controller.java | 28 +++-- .../auctionv2/dto/AuctionRegisterType.java | 21 ++++ .../dto/AuctionRegistrationEvent.java | 9 ++ .../auctionv2/dto/ImageUploadEvent.java | 8 ++ .../dto/request/RegisterRequest.java | 32 +++++ .../domain/auctionv2/entity/AuctionV2.java | 13 ++ .../auctionv2/schedule/AuctionV2EndJob.java | 22 ++++ .../auctionv2/service/AuctionEndService.java | 89 ++++++++++++++ .../service/AuctionRegistrationService.java | 56 +++++++++ .../service/AuctionSchedulingService.java | 49 ++++++++ .../service/AuctionStartService.java | 2 + .../PreAuctionRegistrationService.java | 49 ++++++++ .../service/RegistrationService.java | 9 ++ .../domain/image/error/ImageErrorCode.java | 2 + .../domain/image/service/S3ImageUploader.java | 30 +++++ .../imagev2/repository/ImageV2Repository.java | 7 ++ .../imagev2/service/ImageV2Service.java | 78 ++++++++++++ .../controller/AuctionV2ControllerTest.java | 113 ++++++++++++++++++ .../market/util/AuthenticatedRequestTest.java | 48 ++++++++ 23 files changed, 760 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/chzz/market/common/util/ApplicationContextProvider.java create mode 100644 src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java create mode 100644 src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java create mode 100644 src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java create mode 100644 src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java create mode 100644 src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java create mode 100644 src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java diff --git a/src/main/java/org/chzz/market/common/util/ApplicationContextProvider.java b/src/main/java/org/chzz/market/common/util/ApplicationContextProvider.java new file mode 100644 index 00000000..dc7d89ad --- /dev/null +++ b/src/main/java/org/chzz/market/common/util/ApplicationContextProvider.java @@ -0,0 +1,21 @@ +package org.chzz.market.common.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class ApplicationContextProvider implements ApplicationContextAware { + + private static ApplicationContext context; + + @Override + public void setApplicationContext(ApplicationContext ctx) throws BeansException { + context = ctx; + } + + public static T getBean(Class clazz) { + return context.getBean(clazz); + } +} diff --git a/src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java b/src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java new file mode 100644 index 00000000..588575de --- /dev/null +++ b/src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java @@ -0,0 +1,34 @@ +package org.chzz.market.common.validation.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.chzz.market.common.validation.annotation.NotEmptyMultipartList.List; +import org.chzz.market.common.validation.validator.NotEmptyMultipartListValidator; + +@Documented +@Constraint( + validatedBy = {NotEmptyMultipartListValidator.class} +) +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(List.class) +public @interface NotEmptyMultipartList { + String message() default "파일은 최소 하나 이상 필요합니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface List { + NotEmptyMultipartList[] value(); + } +} diff --git a/src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java b/src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java new file mode 100644 index 00000000..50d3068e --- /dev/null +++ b/src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java @@ -0,0 +1,22 @@ +package org.chzz.market.common.validation.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.Collection; +import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; +import org.springframework.web.multipart.MultipartFile; + +public class NotEmptyMultipartListValidator implements + ConstraintValidator> { + + @Override + public boolean isValid(final Collection multipartFiles, + final ConstraintValidatorContext context) { + for (MultipartFile file : multipartFiles) { + if (file.isEmpty()) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java index 98bdf8c3..b4113010 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java @@ -1,5 +1,7 @@ package org.chzz.market.domain.auctionv2.controller; +import static org.chzz.market.domain.user.error.UserErrorCode.Const.USER_NOT_FOUND; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -8,15 +10,19 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import java.util.List; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; +import org.chzz.market.common.springdoc.ApiExceptionExplanation; +import org.chzz.market.common.springdoc.ApiResponseExplanations; +import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; +import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.user.error.UserErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -92,10 +98,24 @@ ResponseEntity> getUserLikesAuctionList(@LoginUser Long userId, @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); @Operation(summary = "경매 등록", description = "경매를 등록합니다.") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = UserErrorCode.class, constant = USER_NOT_FOUND, name = "회원정보 조회 실패"), + } + ) @PostMapping - ResponseEntity registerAuction(@LoginUser Long userId, - @RequestPart("request") @Valid BaseRegisterRequest request, - @RequestPart(value = "images") List images); + ResponseEntity registerAuction(@LoginUser + Long userId, + + @RequestPart("request") + @Valid + RegisterRequest request, + + @RequestPart(value = "images") + @Valid + @NotEmptyMultipartList + @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") + List images); @Operation(summary = "경매 테스트 등록", description = "테스트 등록합니다.") @PostMapping("/test") diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java index f3f8acd5..dd7f8b0e 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java @@ -1,11 +1,13 @@ package org.chzz.market.domain.auctionv2.controller; import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import java.util.List; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; +import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; +import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; +import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; @@ -16,6 +18,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -120,11 +123,22 @@ public ResponseEntity> getUserLikesAuctionList(@LoginUser Long userId, * 경매 등록 */ @Override - @PostMapping - public ResponseEntity registerAuction(@LoginUser Long userId, - @RequestPart("request") @Valid BaseRegisterRequest request, - @RequestPart(value = "images") List images) { - return null; + @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity registerAuction(@LoginUser + Long userId, + + @RequestPart("request") + @Valid + RegisterRequest request, + + @RequestPart(value = "images") + @Valid + @NotEmptyMultipartList + @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") + List images) { + AuctionRegisterType type = request.auctionRegisterType(); + type.getService().register(userId, request, images);//요청 타입에 따라 다른 서비스 호출 + return ResponseEntity.status(HttpStatus.CREATED).build(); } /** diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java new file mode 100644 index 00000000..c0f32226 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java @@ -0,0 +1,21 @@ +package org.chzz.market.domain.auctionv2.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.chzz.market.common.util.ApplicationContextProvider; +import org.chzz.market.domain.auctionv2.service.AuctionRegistrationService; +import org.chzz.market.domain.auctionv2.service.PreAuctionRegistrationService; +import org.chzz.market.domain.auctionv2.service.RegistrationService; + +@Getter +@AllArgsConstructor +public enum AuctionRegisterType { + PRE_REGISTER(PreAuctionRegistrationService.class), + REGISTER(AuctionRegistrationService.class); + + private final Class serviceClass; + + public RegistrationService getService() { + return ApplicationContextProvider.getBean(serviceClass); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java new file mode 100644 index 00000000..09f20600 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java @@ -0,0 +1,9 @@ +package org.chzz.market.domain.auctionv2.dto; + +import java.time.LocalDateTime; + +public record AuctionRegistrationEvent( + Long auctionId, + LocalDateTime endDateTime +) { +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java new file mode 100644 index 00000000..97a8be5e --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java @@ -0,0 +1,8 @@ +package org.chzz.market.domain.auctionv2.dto; + +import java.util.List; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.springframework.web.multipart.MultipartFile; + +public record ImageUploadEvent(AuctionV2 auction, List images) { +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java new file mode 100644 index 00000000..b49a9ab7 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java @@ -0,0 +1,32 @@ +package org.chzz.market.domain.auctionv2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import org.chzz.market.common.validation.annotation.ThousandMultiple; +import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; +import org.chzz.market.domain.auctionv2.entity.Category; + +public record RegisterRequest( + String productName, + + @Schema(description = "개행문자 포함 최대 1000자, 개행문자 최대 10개") + @Size(max = 1000, message = "상품설명은 1000자 이내여야 합니다.") + @Pattern(regexp = DESCRIPTION_REGEX, message = "줄 바꿈 10번까지 가능합니다") + String description, + + @NotNull(message = "카테고리를 선택해주세요") + Category category, + + @NotNull + @ThousandMultiple + @Max(value = 2_000_000, message = "최소금액은 200만원을 넘을 수 없습니다") + Integer minPrice, + + @NotNull(message = "경매 타입을 선택해주세요") + AuctionRegisterType auctionRegisterType +) { + private static final String DESCRIPTION_REGEX = "^(?:(?:[^\\n]*\\n){0,10}[^\\n]*$)"; // 개행문자 10개를 제한 +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java index e31fedaa..92af397c 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java @@ -131,6 +131,7 @@ public void startOfficialAuction() { throw new AuctionException(AUCTION_ALREADY_OFFICIAL); } this.status = PROCEEDING; + this.endDateTime = LocalDateTime.now().plusDays(1); } public String getFirstImageCdnPath() { @@ -154,4 +155,16 @@ public void validateAuctionEndTime() { public boolean isAboveMinPrice(Long amount) { return amount >= minPrice; } + + public void addImages(final List images) { + this.images.addAll(images); + } + + public void endAuction() { + this.status = ENDED; + } + + public void assignWinner(final Long bidderId) { + this.winnerId = bidderId; + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java b/src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java new file mode 100644 index 00000000..78aee803 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java @@ -0,0 +1,22 @@ +package org.chzz.market.domain.auctionv2.schedule; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.service.AuctionEndService; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.springframework.stereotype.Component; + +/** + * 경매 스케줄링 종료 작업 + */ +@Component +@RequiredArgsConstructor +public class AuctionV2EndJob implements Job { + private final AuctionEndService auctionEndService; + + @Override + public void execute(JobExecutionContext context) { + Long auctionId = context.getJobDetail().getJobDataMap().getLong("auctionId"); + auctionEndService.endAuction(auctionId); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java new file mode 100644 index 00000000..78b644bd --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java @@ -0,0 +1,89 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_FAILURE; +import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_NON_WINNER; +import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_SUCCESS; +import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_WINNER; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.bid.entity.Bid; +import org.chzz.market.domain.bid.repository.BidQueryRepository; +import org.chzz.market.domain.notification.event.NotificationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionEndService { + private final AuctionV2Repository auctionV2Repository; + private final BidQueryRepository bidRepository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public void endAuction(final Long auctionId) { + AuctionV2 auction = auctionV2Repository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AuctionErrorCode.AUCTION_NOT_FOUND)); + + auction.endAuction(); + notifyAuctionEnded(auction); + } + + /** + * 판매자에게 경매 종료 알림 + */ + private void notifyAuctionEnded(AuctionV2 auction) { + Long sellerId = auction.getSeller().getId(); + String productName = auction.getName(); + String firstImageCdnPath = auction.getFirstImageCdnPath(); + List bids = bidRepository.findAllBidsByAuction(auction); + + if (bids.isEmpty()) { // 입찰이 없는 경우 + eventPublisher.publishEvent( + NotificationEvent.createSimpleNotification(sellerId, AUCTION_FAILURE, + AUCTION_FAILURE.getMessage(productName), + firstImageCdnPath)); // 낙찰 실패 알림 이벤트 + return; + } + eventPublisher.publishEvent( + NotificationEvent.createAuctionNotification(sellerId, AUCTION_SUCCESS, + AUCTION_SUCCESS.getMessage(productName), + firstImageCdnPath, auction.getId())); // 낙찰 성공 알림 이벤트 + + alter2Winner(auction, bids.get(0), productName, firstImageCdnPath); // 첫 번째 입찰이 낙찰 + notify2NonWinner(bids, productName, firstImageCdnPath); + + } + + /** + * 낙찰자에게 알림 전송 + */ + private void alter2Winner(AuctionV2 auction, Bid winningBid, String productName, String firstImageCdnPath) { + auction.assignWinner(winningBid.getBidderId()); + eventPublisher.publishEvent( + NotificationEvent.createAuctionNotification(winningBid.getBidderId(), AUCTION_WINNER, + AUCTION_WINNER.getMessage(productName), firstImageCdnPath, auction.getId())); // 낙찰자 알림 이벤트 + log.info("경매 ID {}: 낙찰자 처리 완료", auction.getId()); + } + + /** + * 미낙찰자들에게 알림 전송 + */ + private void notify2NonWinner(List bids, String productName, String firstImageCdnPath) { + List nonWinnerIds = bids.stream().skip(1) // 낙찰자를 제외한 나머지 입찰자들 + .map(Bid::getBidderId).collect(Collectors.toList()); + + if (!nonWinnerIds.isEmpty()) { + eventPublisher.publishEvent(NotificationEvent.createSimpleNotification(nonWinnerIds, AUCTION_NON_WINNER, + AUCTION_NON_WINNER.getMessage(productName), firstImageCdnPath)); // 미낙찰자 알림 이벤트 + } + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java new file mode 100644 index 00000000..438ea973 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java @@ -0,0 +1,56 @@ +package org.chzz.market.domain.auctionv2.service; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.dto.AuctionRegistrationEvent; +import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; +import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.error.UserErrorCode; +import org.chzz.market.domain.user.error.exception.UserException; +import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuctionRegistrationService implements RegistrationService { + private final AuctionV2Repository auctionRepository; + private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; + + @Override + @Transactional + public void register(final Long userId, RegisterRequest request, final List images) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + + AuctionV2 auction = createAuction(request, user); + + auctionRepository.save(auction); + + eventPublisher.publishEvent(new ImageUploadEvent(auction, images)); + eventPublisher.publishEvent(new AuctionRegistrationEvent(auction.getId(),auction.getEndDateTime())); + } + + + private AuctionV2 createAuction(final RegisterRequest request, final User user) { + return AuctionV2.builder() + .name(request.productName()) + .minPrice(request.minPrice()) + .description(request.description()) + .category(request.category()) + .seller(user) + .status(AuctionStatus.PROCEEDING) + .endDateTime(LocalDateTime.now().plusDays(1)) + .build(); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java new file mode 100644 index 00000000..f03f71a1 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java @@ -0,0 +1,49 @@ +package org.chzz.market.domain.auctionv2.service; + +import java.sql.Date; +import java.time.LocalDateTime; +import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.dto.AuctionRegistrationEvent; +import org.chzz.market.domain.auctionv2.schedule.AuctionV2EndJob; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleTrigger; +import org.quartz.TriggerBuilder; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionSchedulingService { + private final Scheduler scheduler; + + @EventListener + public void registerSchedule(AuctionRegistrationEvent event) { + Long auctionId = event.auctionId(); + LocalDateTime endDateTime = event.endDateTime(); + // Job과 Trigger를 스케줄러에 등록 + try { + // JobDetail 생성 + JobDetail jobDetail = JobBuilder.newJob(AuctionV2EndJob.class) + .withIdentity("auctionEndJob_" + auctionId, "auctionJobs") + .usingJobData("auctionId", String.valueOf(auctionId)) // auctionId를 문자열로 변환하여 저장 + .build(); + + // Trigger 생성 + SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() + .withIdentity("auctionEndTrigger_" + auctionId, "auctionTriggers") + .startAt(Date.from(endDateTime.atZone(ZoneId.systemDefault()).toInstant())) + .build(); + scheduler.scheduleJob(jobDetail, trigger); + log.info("Scheduled job with ID: {} and Trigger: {} at {}", jobDetail.getKey(), trigger.getKey(), + endDateTime); + } catch (SchedulerException e) { + log.error("SchedulerException occurred while scheduling job", e); + } + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java index 153ca2d8..ebc09cee 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java @@ -6,6 +6,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.dto.AuctionRegistrationEvent; import org.chzz.market.domain.auctionv2.entity.AuctionV2; import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; @@ -33,6 +34,7 @@ public void start(Long userId, Long auctionId) { .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); auction.validateOwner(userId); auction.startOfficialAuction(); + eventPublisher.publishEvent(new AuctionRegistrationEvent(auction.getId(), auction.getEndDateTime())); processStartNotification(auction); log.info("{}번 경매 정식경매 전환완료", auctionId); } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java new file mode 100644 index 00000000..9684f900 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java @@ -0,0 +1,49 @@ +package org.chzz.market.domain.auctionv2.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; +import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.error.UserErrorCode; +import org.chzz.market.domain.user.error.exception.UserException; +import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class PreAuctionRegistrationService implements RegistrationService { + private final AuctionV2Repository auctionRepository; + private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; + + @Override + @Transactional + public void register(final Long userId, RegisterRequest request, final List images) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + + AuctionV2 auction = createAuction(request, user); + + auctionRepository.save(auction); + + eventPublisher.publishEvent(new ImageUploadEvent(auction,images)); + } + + private AuctionV2 createAuction(final RegisterRequest request, final User user) { + return AuctionV2.builder() + .name(request.productName()) + .minPrice(request.minPrice()) + .category(request.category()) + .description(request.description()) + .seller(user) + .status(AuctionStatus.PRE) + .build(); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java new file mode 100644 index 00000000..5f2ef194 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java @@ -0,0 +1,9 @@ +package org.chzz.market.domain.auctionv2.service; + +import java.util.List; +import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; +import org.springframework.web.multipart.MultipartFile; + +public interface RegistrationService { + void register(Long userId, RegisterRequest request, List images); +} diff --git a/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java b/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java index f0bb886e..dd0f1765 100644 --- a/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java +++ b/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java @@ -10,6 +10,7 @@ public enum ImageErrorCode implements ErrorCode { IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드를 실패했습니다."), IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다. "), + IMAGE_CONVERSION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 변환에 실패했습니다."), INVALID_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 확장자입니다."), MAX_IMAGE_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지는 최대 5개까지 등록할 수 있습니다."), INVALID_IMAGE_COUNT(HttpStatus.BAD_REQUEST, "이미지 개수가 올바르지 않습니다."), @@ -23,6 +24,7 @@ public enum ImageErrorCode implements ErrorCode { public static class Const { public static final String IMAGE_UPLOAD_FAILED = "IMAGE_UPLOAD_FAILED"; public static final String IMAGE_DELETE_FAILED = "IMAGE_DELETE_FAILED"; + public static final String IMAGE_CONVERSION_FAILURE = "IMAGE_CONVERSION_FAILURE"; public static final String INVALID_IMAGE_EXTENSION = "INVALID_IMAGE_EXTENSION"; public static final String MAX_IMAGE_COUNT_EXCEEDED = "MAX_IMAGE_COUNT_EXCEEDED"; public static final String INVALID_IMAGE_COUNT = "INVALID_IMAGE_COUNT"; diff --git a/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java b/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java index ed248b82..1e67a1f5 100644 --- a/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java +++ b/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java @@ -3,6 +3,9 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.chzz.market.domain.image.error.ImageErrorCode; @@ -15,6 +18,8 @@ @Service @RequiredArgsConstructor public class S3ImageUploader implements ImageUploader { + private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); + private final AmazonS3 amazonS3Client; @Value("${cloud.aws.s3.bucket}") @@ -34,4 +39,29 @@ public String uploadImage(MultipartFile image, String fileName) { throw new ImageException(ImageErrorCode.IMAGE_UPLOAD_FAILED); } } + + public List uploadImages(final Map multipartFiles) { + return multipartFiles.entrySet().stream() + .map(entry -> uploadImage(entry.getValue(), entry.getKey())) + .toList(); + } + +// public List uploadImages(List images) { +// TransferManager transfer = TransferManagerBuilder.standard().withS3Client(amazonS3Client).build(); +// MultipleFileUpload upload = transfer.uploadFileList(bucket, "", new File("."), images); +// +// return upload.getSubTransfers().stream().map(this::getFileName).toList(); +// } +// +// /** +// * @return 각 파일의 key값(파일명) +// */ +// private String getFileName(final Upload subUpload) { +// try { +// UploadResult uploadResult = subUpload.waitForUploadResult(); +// return uploadResult.getKey(); +// } catch (InterruptedException e) { +// throw new ImageException(ImageErrorCode.IMAGE_UPLOAD_FAILED); +// } +// } } diff --git a/src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java b/src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java new file mode 100644 index 00000000..e44c291e --- /dev/null +++ b/src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java @@ -0,0 +1,7 @@ +package org.chzz.market.domain.imagev2.repository; + +import org.chzz.market.domain.image.entity.ImageV2; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ImageV2Repository extends JpaRepository { +} diff --git a/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java b/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java new file mode 100644 index 00000000..258c1de8 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java @@ -0,0 +1,78 @@ +package org.chzz.market.domain.imagev2.service; + +import static org.chzz.market.domain.image.error.ImageErrorCode.INVALID_IMAGE_EXTENSION; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.image.error.exception.ImageException; +import org.chzz.market.domain.image.service.S3ImageUploader; +import org.chzz.market.domain.imagev2.repository.ImageV2Repository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class ImageV2Service { + private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); + + private final ImageV2Repository imageRepository; + private final S3ImageUploader s3ImageUploader; + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void uploadImages(final ImageUploadEvent event) { + Map buffer = setImageBuffer(event); + + List paths = s3ImageUploader.uploadImages(buffer); + + AuctionV2 auction = event.auction(); + + List list = paths.stream() + .map(path -> createImage(path, auction)).toList(); + + auction.addImages(list); + + imageRepository.saveAll(list); + } + + private Map setImageBuffer(final ImageUploadEvent event) { + Map imageBuffer = new HashMap<>(); + for (MultipartFile image : event.images()) { + String originalFilename = image.getOriginalFilename(); + String uniqueFileName = createUniqueFileName(originalFilename); + imageBuffer.put(uniqueFileName, image); + } + return imageBuffer; + } + + private ImageV2 createImage(final String path, final AuctionV2 auction) { + return ImageV2.builder() + .auction(auction) + .cdnPath(path) + .build(); + } + + private String createUniqueFileName(String originalFileName) { + String uuid = UUID.randomUUID().toString(); + String extension = StringUtils.getFilenameExtension(originalFileName); + + if (extension == null || !isValidFileExtension(extension)) { + throw new ImageException(INVALID_IMAGE_EXTENSION); + } + + return uuid + "." + extension; + } + + private boolean isValidFileExtension(String extension) { + return ALLOWED_EXTENSIONS.contains(extension.toLowerCase()); + } +} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java b/src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java new file mode 100644 index 00000000..a5c43e3d --- /dev/null +++ b/src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java @@ -0,0 +1,113 @@ +package org.chzz.market.domain.auctionv2.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; +import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.imagev2.service.ImageV2Service; +import org.chzz.market.util.AuthenticatedRequestTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + + +class AuctionV2ControllerTest extends AuthenticatedRequestTest { + @MockBean + ImageV2Service imageV2Service; + + RegisterRequest request; + + MockMultipartFile image1,image2,image3,image4,image5,image6; + MockMultipartFile requestPart; + + @BeforeEach + void setUp() throws JsonProcessingException { + request = new RegisterRequest("name", "description", Category.BOOKS_AND_MEDIA, 10000, + AuctionRegisterType.PRE_REGISTER); + requestPart = new MockMultipartFile( + "request", "request", "application/json", objectMapper.writeValueAsBytes(request) + ); + + image1 = new MockMultipartFile("images", "imagefile1.jpeg", "image/jpeg", + "<>".getBytes()); + image2 = new MockMultipartFile("images", "imagefile2.jpeg", "image/jpeg", + "<>".getBytes()); + + image3 = new MockMultipartFile("images", "imagefile3.jpeg", "image/jpeg", + "<>".getBytes()); + image4 = new MockMultipartFile("images", "imagefile4.jpeg", "image/jpeg", + "<>".getBytes()); + + image5 = new MockMultipartFile("images", "imagefile5.jpeg", "image/jpeg", + "<>".getBytes()); + image6 = new MockMultipartFile("images", "imagefile6.jpeg", "image/gif", + "<>".getBytes()); + } + + @Test + @DisplayName("사전 경매 등록") + void testPreAuctionRegistration() throws Exception { + // when + mockMvc.perform(multipart("/api/v2/auctions") + .file(requestPart) + .file(image1) + .file(image2) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isCreated()) + .andDo(print()); + + verify(imageV2Service).uploadImages(any()); + } + + @Test + @DisplayName("이미지가 없는 경우") + void testRegisterAuctionWithNoImage() throws Exception { + + MockMultipartFile emptyImage = new MockMultipartFile( + "images", "file", MediaType.MULTIPART_FORM_DATA_VALUE, new byte[0] + ); + // when + mockMvc.perform(multipart("/api/v2/auctions") + .file(emptyImage) + .file(requestPart) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message[0]").value(containsString("images: 파일은 최소 하나 이상 필요합니다."))) + .andDo(print()); + + } + + @Test + @DisplayName("이미지가 5개 이상인 경우") + void testRegisterAuctionWithOverImageCount() throws Exception { + // given + mockMvc.perform(multipart("/api/v2/auctions") + .file(requestPart) + .file(image1) + .file(image2) + .file(image3) + .file(image4) + .file(image5) + .file(image6) + .accept(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message[0]").value(containsString("images: 이미지는 5장 이내로만 업로드 가능합니다."))) + .andDo(print()); + } +} diff --git a/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java b/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java new file mode 100644 index 00000000..321ae570 --- /dev/null +++ b/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java @@ -0,0 +1,48 @@ +package org.chzz.market.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.UUID; +import org.chzz.market.domain.user.dto.CustomUserDetails; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@AutoConfigureMockMvc +@SpringBootTest +public class AuthenticatedRequestTest { + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected UserRepository userRepository; + + @BeforeEach + @Transactional + void setUpMocks() { + User mockUser = User.builder() + .id(1L) + .email("jake@naver.com") + .nickname("jake") + .providerId("12345678") + .customerKey(UUID.randomUUID()) + .build(); + + userRepository.save(mockUser); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + new CustomUserDetails(mockUser), null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} From 16f0403ea3f0858bba5c683468e166bbe729e067 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Sun, 24 Nov 2024 21:00:12 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A2=8B=EC=95=84=EC=9A=94=ED=95=9C=20=EC=82=AC?= =?UTF-8?q?=EC=A0=84=EA=B2=BD=EB=A7=A4=EB=AA=A9=EB=A1=9D=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?,=20=EB=A7=88=EA=B0=90=EC=9E=84=EB=B0=95=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20=EC=A0=84=ED=99=98=20(#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 마감임박 조회 API 전환 추가 * feat: ConstraintViolationException 예외 핸들러 추가 * refactor: 마감임박 조회 서비스 함수 로직 추가 * feat: 마감임박 조회 파라미터 유효성 예외 추가 * feat: 마감임박 조회 쿼리 변경 * test: 마감임박 조회 쿼리 변경으로 인한 테스트 코드 수정 * test: 마감임박 조회 쿼리 테스트 추가 * feat: 사용자가 좋아요(찜)한 경매 목록 조회 api 전환 * feat: 사용자가 좋아요(찜)한 경매 목록 조회 쿼리 추가 * feat: 사용자가 좋아요(찜)한 경매 목록 조회 쿼리 테스트 추가 * feat: 사용자가 좋아요(찜)한 경매 목록 조회 서비스 함수 추가 * feat: 사용자가 등록한 사전 경매목록 조회 API 전환 * feat: 사용자가 등록한 사전 경매목록 조회 서비스 함수 추가 * feat: 사용자가 등록한 사전 경매목록 조회 쿼리 추가 * test: 사용자가 등록한 사전 경매목록 조회 쿼리 테스트 추가 * test: 마감 임박 조회시 사전경매를 조회 예외 테스트 * fix: 중복된 from 키워드 삭제 * refactor: baseQuery로 중복된 부분 추출 * fix: join 전략 수정 --- .../error/handler/GlobalExceptionHandler.java | 21 +++ .../auctionv2/controller/AuctionV2Api.java | 24 ++-- .../controller/AuctionV2Controller.java | 33 ++--- .../auctionv2/error/AuctionErrorCode.java | 3 + .../repository/AuctionV2QueryRepository.java | 97 ++++++++++++-- .../service/AuctionLookupService.java | 13 +- .../auctionv2/service/AuctionMyService.java | 31 +++++ .../service/AuctionRegistrationService.java | 2 +- .../PreAuctionRegistrationService.java | 2 +- .../controller/AuctionV2ControllerTest.java | 2 +- .../AuctionV2QueryRepositoryTest.java | 121 +++++++++++++++++- .../service/AuctionLookupServiceTest.java | 29 +++++ 12 files changed, 329 insertions(+), 49 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java create mode 100644 src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java diff --git a/src/main/java/org/chzz/market/common/error/handler/GlobalExceptionHandler.java b/src/main/java/org/chzz/market/common/error/handler/GlobalExceptionHandler.java index 1a4688cc..fab718d5 100644 --- a/src/main/java/org/chzz/market/common/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/org/chzz/market/common/error/handler/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import static org.chzz.market.common.error.GlobalErrorCode.EXTERNAL_API_ERROR; +import jakarta.validation.ConstraintViolationException; import java.io.IOException; import java.util.List; import lombok.NonNull; @@ -12,6 +13,7 @@ import org.chzz.market.common.error.GlobalErrorCode; import org.chzz.market.common.error.GlobalException; import org.chzz.market.common.error.exception.BusinessException; +import org.hibernate.validator.internal.engine.path.PathImpl; import org.springframework.context.MessageSourceResolvable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -86,6 +88,25 @@ public ResponseEntity handleIOException(IOException e) { } } + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolation(ConstraintViolationException ex) { + GlobalErrorCode errorCode = GlobalErrorCode.VALIDATION_FAILED; + + String[] errorMessages = ex.getConstraintViolations().stream() + .map(violation -> { + String field = ((PathImpl) violation.getPropertyPath()).getLeafNode().toString(); + String message = violation.getMessage(); + return String.format("%s: %s", field, message); + }) + .toArray(String[]::new); // List가 아닌 배열로 변환 + logException(ex, errorCode, errorMessages); + + ErrorResponse errorResponse = ErrorResponse.of(errorCode, errorMessages); + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(errorResponse); + } + // 2. ResponseEntityExceptionHandler에서 오버라이드된 핸들러 /** diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java index b4113010..509cd01d 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java @@ -1,6 +1,7 @@ package org.chzz.market.domain.auctionv2.controller; import static org.chzz.market.domain.user.error.UserErrorCode.Const.USER_NOT_FOUND; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -11,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.Min; import java.util.List; import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; @@ -23,6 +25,7 @@ import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.user.error.UserErrorCode; +import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -53,20 +56,21 @@ public interface AuctionV2Api { )} ) }) + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY, name = "minutes 파라미터는 진행중인 경매일 때만 사용가능"), + } + ) @GetMapping ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(required = false) Category category, @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + @RequestParam(required = false) @Min(value = 1, message = "minutes는 1 이상의 값이어야 합니다.") Integer minutes, + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "경매 카테고리 조회", description = "경매 카테고리 목록을 조회합니다.") @GetMapping("/categories") ResponseEntity> getCategoryList(); - @Operation(summary = "마감임박 조회", description = "정식 경매의 마감임박") - @GetMapping("/imminent") - ResponseEntity> getImminentAuctionList( - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); - @Operation(summary = "사용자가 등록한 진행중인 경매 목록 조회", description = "사용자가 등록한 진행중인 경매 목록을 조회합니다.") @GetMapping("/users/proceeding") ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, @@ -79,8 +83,8 @@ ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, @Operation(summary = "사용자가 등록한 사전 경매 목록 조회", description = "사용자가 등록한 사전 경매 목록을 조회합니다.") @GetMapping("/users/pre") - ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "사용자가 낙찰한 경매 목록 조회", description = "사용자가 낙찰한 경매 목록을 조회합니다.") @GetMapping("/users/won") @@ -94,8 +98,8 @@ ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, @Operation(summary = "사용자가 좋아요(찜)한 경매 목록 조회", description = "사용자가 좋아요(찜)한 경매 목록을 조회합니다.") @GetMapping("/users/likes") - ResponseEntity> getUserLikesAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + ResponseEntity> getLikedAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); @Operation(summary = "경매 등록", description = "경매를 등록합니다.") @ApiResponseExplanations( diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java index dd7f8b0e..5881f365 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java @@ -1,6 +1,7 @@ package org.chzz.market.domain.auctionv2.controller; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Size; import java.util.List; import lombok.RequiredArgsConstructor; @@ -9,10 +10,12 @@ import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.auctionv2.service.AuctionCategoryService; import org.chzz.market.domain.auctionv2.service.AuctionLookupService; +import org.chzz.market.domain.auctionv2.service.AuctionMyService; import org.chzz.market.domain.auctionv2.service.AuctionTestService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -20,6 +23,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -29,10 +33,12 @@ @RestController @RequiredArgsConstructor +@Validated public class AuctionV2Controller implements AuctionV2Api { private final AuctionLookupService auctionLookupService; private final AuctionCategoryService auctionCategoryService; private final AuctionTestService testService; + private final AuctionMyService auctionMyService; /** * 경매 목록 조회 @@ -42,8 +48,10 @@ public class AuctionV2Controller implements AuctionV2Api { public ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(required = false) Category category, @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, + @RequestParam(required = false) @Min(value = 1, message = "minutes는 1 이상의 값이어야 합니다.") Integer minutes, @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionLookupService.getAuctionList(userId, category, status, pageable)); + return ResponseEntity.ok( + auctionLookupService.getAuctionList(userId, category, status, minutes, pageable)); } /** @@ -55,15 +63,6 @@ public ResponseEntity> getCategoryList() { return ResponseEntity.ok(auctionCategoryService.getCategories()); } - /** - * 정식 경매의 마감임박 조회 - */ - @Override - @GetMapping("/imminent") - public ResponseEntity> getImminentAuctionList(@PageableDefault(sort = "newest") Pageable pageable) { - return null; - } - /** * 사용자가 등록한 진행중인 경매 목록 조회 */ @@ -87,9 +86,10 @@ public ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, * 사용자가 등록한 사전 경매 목록 조회 */ @Override - public ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + @GetMapping("/users/pre") + public ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserPreAuctionList(userId, pageable)); } /** @@ -114,9 +114,10 @@ public ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, * 사용자가 좋아요(찜)한 경매 목록 조회 */ @Override - public ResponseEntity> getUserLikesAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + @GetMapping("/users/likes") + public ResponseEntity> getLikedAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getLikedAuctionList(userId, pageable)); } /** diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java index 2f18c98a..a5691bf5 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java @@ -15,6 +15,8 @@ public enum AuctionErrorCode implements ErrorCode { AUCTION_NOT_ENDED(BAD_REQUEST, "해당 경매가 아직 끝나지 않았습니다."), AUCTION_ALREADY_OFFICIAL(BAD_REQUEST, "해당 경매는 이미 정식 경매입니다."), AUCTION_ENDED(BAD_REQUEST, "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), + END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY(BAD_REQUEST, + "진행중인 경매 목록 조회 시에만 minutes 파라미터를 사용할 수 있습니다."), OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), NOW_WINNER(FORBIDDEN, "낙찰자가 아닙니다."), AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), @@ -27,6 +29,7 @@ public static class Const { public static final String AUCTION_NOT_ENDED = "AUCTION_NOT_ENDED"; public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; public static final String AUCTION_ENDED = "AUCTION_ENDED"; + public static final String END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY = "END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY"; public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; public static final String NOW_WINNER = "NOW_WINNER"; public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java index 0c6e0fc4..3e9f6acc 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java @@ -4,7 +4,6 @@ import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PRE; -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PROCEEDING; import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; @@ -17,6 +16,8 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -149,7 +150,10 @@ public Optional findOfficialAuctionDetailById(Lon * 사전 경매 목록 조회 */ public Page findPreAuctions(Long userId, Category category, Pageable pageable) { - List content = jpaQueryFactory.from(auctionV2) + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PRE))); + + List content = baseQuery .select( Projections.constructor( PreAuctionResponse.class, @@ -162,19 +166,15 @@ public Page findPreAuctions(Long userId, Category category, likeV2.id.isNotNull() ) ) - .from(auctionV2) .join(auctionV2.seller, user) .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) .leftJoin(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeUserIdEq(userId))) - .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PRE))) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = jpaQueryFactory.select(auctionV2.count()) - .from(auctionV2) - .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PRE))); + JPAQuery countQuery = baseQuery.select(auctionV2.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -183,8 +183,13 @@ public Page findPreAuctions(Long userId, Category category, * 정식 경매 목록 조회 */ public Page findOfficialAuctions(Long userId, Category category, AuctionStatus status, + Integer endWithinSeconds, Pageable pageable) { - List content = jpaQueryFactory.from(auctionV2) + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(status)) + .and(timeRemainingIgnoreNull(endWithinSeconds))); + + List content = baseQuery .select( Projections.constructor( OfficialAuctionResponse.class, @@ -198,19 +203,80 @@ public Page findOfficialAuctions(Long userId, Category bid.id.isNotNull() ) ) - .from(auctionV2) .join(auctionV2.seller, user) .leftJoin(bid).on(bid.auctionId.eq(auctionV2.id).and(bidderIdEq(userId)).and(bid.status.eq(ACTIVE))) .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) - .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(status))) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = jpaQueryFactory.select(auctionV2.count()) - .from(auctionV2) - .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PROCEEDING))); + JPAQuery countQuery = baseQuery.select(auctionV2.count()); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + /** + * 사용자가 등록한 사전경매 목록 조회 + */ + public Page findPreAuctionsByUserId(Long userId, Pageable pageable) { + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .join(auctionV2.seller, user).on(user.id.eq(userId)) + .where(auctionV2.status.eq(PRE)); + + List content = baseQuery + .select( + Projections.constructor( + PreAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + Expressions.TRUE, + auctionV2.likeCount, + Expressions.FALSE + ) + ) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery.select(auctionV2.count()); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + /** + * 사용자가 좋아요한 사전 경매목록 조회 + */ + public Page findLikedAuctionsByUserId(Long userId, Pageable pageable) { + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .join(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeV2.userId.eq(userId))) + .where(auctionV2.status.eq(PRE)); + + List content = baseQuery + .select( + Projections.constructor( + PreAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + auctionV2.likeCount, + likeV2.id.isNotNull() + ) + ) + .join(auctionV2.seller, user) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery.select(auctionV2.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -248,6 +314,10 @@ private BooleanBuilder categoryEqIgnoreNull(Category category) { return nullSafeBuilderIgnore(() -> auctionV2.category.eq(category)); } + private BooleanExpression timeRemainingIgnoreNull(Integer endWithinSeconds) { + return endWithinSeconds != null ? timeRemaining().between(0, endWithinSeconds) : null; + } + private static NumberExpression timeRemaining() { return numberTemplate(Integer.class, "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 @@ -259,6 +329,7 @@ public enum AuctionOrder implements QuerydslOrder { POPULARITY("popularity-v2", auctionV2.bidCount.desc()), EXPENSIVE("expensive-v2", auctionV2.minPrice.desc()), CHEAP("cheap-v2", auctionV2.minPrice.asc()), + IMMEDIATELY("immediately-v2", timeRemaining().asc()), NEWEST("newest-v2", auctionV2.createdAt.desc()); private final String name; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java index a836c7f7..75c0dab0 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java @@ -1,8 +1,11 @@ package org.chzz.market.domain.auctionv2.service; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; + import lombok.RequiredArgsConstructor; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -18,10 +21,16 @@ public class AuctionLookupService { /** * 경매 목록 조회 */ - public Page getAuctionList(Long userId, Category category, AuctionStatus status, Pageable pageable) { + public Page getAuctionList(Long userId, Category category, AuctionStatus status, Integer endWithinMinutes, + Pageable pageable) { + if (endWithinMinutes != null && !status.equals(AuctionStatus.PROCEEDING)) { + throw new AuctionException(END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY); + } + Integer endWithinSeconds = endWithinMinutes != null ? endWithinMinutes * 60 : null; return switch (status) { case PRE -> auctionQueryRepository.findPreAuctions(userId, category, pageable); - case PROCEEDING, ENDED -> auctionQueryRepository.findOfficialAuctions(userId, category, status, pageable); + case PROCEEDING, ENDED -> + auctionQueryRepository.findOfficialAuctions(userId, category, status, endWithinSeconds, pageable); }; } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java new file mode 100644 index 00000000..e3cc3665 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java @@ -0,0 +1,31 @@ +package org.chzz.market.domain.auctionv2.service; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AuctionMyService { + private final AuctionV2QueryRepository auctionQueryRepository; + + /** + * 사용자가 등록한 사전 경매 목록 조회 + */ + public Page getUserPreAuctionList(Long userId, Pageable pageable) { + return auctionQueryRepository.findPreAuctionsByUserId(userId, pageable); + } + + /** + * 사용자가 좋아요한 사전 경매 목록 조회 + */ + public Page getLikedAuctionList(Long userId, Pageable pageable) { + return auctionQueryRepository.findLikedAuctionsByUserId(userId, pageable); + } + +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java index 438ea973..5ac57b75 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java @@ -38,7 +38,7 @@ public void register(final Long userId, RegisterRequest request, final List result = auctionQueryRepository.findOfficialAuctions(seller.getId(), - Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + Category.ELECTRONICS, AuctionStatus.PROCEEDING, null, pageable); //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); @@ -276,7 +278,7 @@ class Auctions { //when Page result = auctionQueryRepository.findOfficialAuctions(user.getId(), - Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + Category.ELECTRONICS, AuctionStatus.PROCEEDING, null, pageable); //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); @@ -286,7 +288,7 @@ class Auctions { //when 비로그인 Page result1 = auctionQueryRepository.findOfficialAuctions(null, - Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + Category.ELECTRONICS, AuctionStatus.PROCEEDING, null, pageable); //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); @@ -305,7 +307,7 @@ class Auctions { bidRepository.save(bid); //when Page result = auctionQueryRepository.findOfficialAuctions(user.getId(), - Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + Category.ELECTRONICS, AuctionStatus.PROCEEDING, null, pageable); //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); @@ -376,7 +378,7 @@ class Auctions { //when Page result = auctionQueryRepository.findOfficialAuctions(seller.getId(), - Category.ELECTRONICS, AuctionStatus.PROCEEDING, pageable); + Category.ELECTRONICS, AuctionStatus.PROCEEDING, null, pageable); //then assertThat(result).isNotNull(); @@ -384,5 +386,114 @@ class Auctions { assertThat(result.getContent().get(0).getProductName()).isEqualTo("아이패드"); // 가격이 더 높은 아이패드가 먼저 assertThat(result.getContent().get(1).getProductName()).isEqualTo("맥북프로"); // 가격이 낮은 맥북프로가 나중 } + + @Test + public void 정식경매_목록_조회_종료까지_남은시간_테스트() throws Exception { + // given + AuctionV2 auction1 = AuctionV2.builder() + .seller(seller) + .name("맥북프로") + .description("맥북프로 2019년형 팝니다.") + .status(AuctionStatus.PROCEEDING) + .category(Category.ELECTRONICS) + .winnerId(null) + .minPrice(1000) + .endDateTime(LocalDateTime.now().plusSeconds(3600)) // 1시간 뒤 종료 + .build(); + + AuctionV2 auction2 = AuctionV2.builder() + .seller(seller) + .name("아이패드") + .description("아이패드 2019년형 팝니다.") + .status(AuctionStatus.PROCEEDING) + .category(Category.ELECTRONICS) + .winnerId(null) + .minPrice(2000) + .endDateTime(LocalDateTime.now().plusSeconds(7200)) // 2시간 뒤 종료 + .build(); + auctionV2Repository.saveAll(List.of(auction1, auction2)); + Pageable pageable = PageRequest.of(0, 10, Sort.by("immediately-v2")); + + Page resultWithin1Hour = auctionQueryRepository.findOfficialAuctions(null, null, + AuctionStatus.PROCEEDING, 3600, pageable); + + // then + assertThat(resultWithin1Hour).isNotNull(); + assertThat(resultWithin1Hour.getContent()).hasSize(1); + assertThat(resultWithin1Hour.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + + // when - endWithinSeconds 2시간 이내 + Page resultWithin2Hours = auctionQueryRepository.findOfficialAuctions( + seller.getId(), + Category.ELECTRONICS, + AuctionStatus.PROCEEDING, + 7200, // 2시간 이내 + pageable + ); + + // then + assertThat(resultWithin2Hours).isNotNull(); + assertThat(resultWithin2Hours.getContent()).hasSize(2); + assertThat(resultWithin2Hours.getContent().get(0).getProductName()).isEqualTo("맥북프로"); // 더 빨리 종료되는 맥북 + assertThat(resultWithin2Hours.getContent().get(1).getProductName()).isEqualTo("아이패드"); // 나중에 종료되는 아이패드 + } + } + + @Nested + @DisplayName("나의 경매 목록 조회") + class MyAuctions { + @Test + void 내가_좋아요한_사전경매_목록조회() { + // Given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionV2Repository.save(auction1); + auctionV2Repository.save(auction2); + + LikeV2 like1 = LikeV2.builder().auctionId(auction1.getId()).userId(user.getId()).build(); + LikeV2 like2 = LikeV2.builder().auctionId(auction2.getId()).userId(user.getId()).build(); + likeV2Repository.save(like1); + likeV2Repository.save(like2); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + // When + Page result = auctionQueryRepository.findLikedAuctionsByUserId(user.getId(), pageable); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getProductName()).isEqualTo("아이패드"); + assertThat(result.getContent().get(1).getProductName()).isEqualTo("맥북프로"); + } + + + @Test + void 사용자가_등록한_사전경매_목록_조회() { + // Given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + AuctionV2 auction3 = createAuction(user, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionV2Repository.saveAll(List.of(auction1, auction2, auction3)); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + // When + Page result = auctionQueryRepository.findPreAuctionsByUserId(seller.getId(), pageable); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + + PreAuctionResponse response1 = result.getContent().get(0); + assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getIsSeller()).isTrue(); + assertThat(response1.getIsLiked()).isFalse(); + + PreAuctionResponse response2 = result.getContent().get(1); + assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getIsSeller()).isTrue(); + assertThat(response2.getIsLiked()).isFalse(); + } } } diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java new file mode 100644 index 00000000..5aa1e8b3 --- /dev/null +++ b/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java @@ -0,0 +1,29 @@ +package org.chzz.market.domain.auctionv2.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; + +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; + +@SpringBootTest +class AuctionLookupServiceTest { + @Autowired + AuctionLookupService auctionLookupService; + + @Test + void 남은시간_파라미터를_사전경매조회에_사용하면_예외가_발생한다() { + // given when + assertThatThrownBy(() -> auctionLookupService.getAuctionList(1L, Category.ELECTRONICS, AuctionStatus.PRE, 1, + PageRequest.of(0, 10))) + .isInstanceOf(AuctionException.class) + .extracting("errorCode") + .isEqualTo(END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY); + } + +} From 0c0d02fd3b537ff8ae7bc706b87f6196d2b7d287 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:01:45 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=EA=B2=BD=EB=A7=A4=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 종료된 경매 목록 조회 응답 dto 추가 * feat: 진행중인 경매 목록 조회 응답 dto 추가 * feat: 낙찰실패 경매 목록 조회 응답 dto 추가 * feat: 낙찰성공 경매 목록 조회 응답 dto 추가 * feat: 각종 API 조회 쿼리 추가 * feat: 각종 API 조회 서비스 함수 추가 * feat: 사용자 경매 API 조회 전환 * test: 사용자 경매 API 조회 테스트 추가 --- .../auctionv2/controller/AuctionV2Api.java | 44 +++-- .../controller/AuctionV2Controller.java | 44 +++-- .../dto/response/EndedAuctionResponse.java | 26 +++ .../dto/response/LostAuctionResponse.java | 21 +++ .../response/ProceedingAuctionResponse.java | 26 +++ .../dto/response/WonAuctionResponse.java | 26 +++ .../repository/AuctionV2QueryRepository.java | 159 ++++++++++++++++++ .../auctionv2/service/AuctionMyService.java | 31 ++++ .../AuctionV2QueryRepositoryTest.java | 148 +++++++++++++++- 9 files changed, 474 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java index 509cd01d..09216498 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java @@ -1,7 +1,7 @@ package org.chzz.market.domain.auctionv2.controller; -import static org.chzz.market.domain.user.error.UserErrorCode.Const.USER_NOT_FOUND; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; +import static org.chzz.market.domain.user.error.UserErrorCode.Const.USER_NOT_FOUND; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -11,8 +11,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; import java.util.List; import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; @@ -20,12 +20,16 @@ import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; +import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.user.error.UserErrorCode; import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; +import org.chzz.market.domain.user.error.UserErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -73,13 +77,13 @@ ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(req @Operation(summary = "사용자가 등록한 진행중인 경매 목록 조회", description = "사용자가 등록한 진행중인 경매 목록을 조회합니다.") @GetMapping("/users/proceeding") - ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "사용자가 등록한 종료된 경매 목록 조회", description = "사용자가 등록한 종료된 경매 목록을 조회합니다.") @GetMapping("/users/ended") - ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "사용자가 등록한 사전 경매 목록 조회", description = "사용자가 등록한 사전 경매 목록을 조회합니다.") @GetMapping("/users/pre") @@ -88,18 +92,18 @@ ResponseEntity> getUserPreAuctionList(@LoginUser Long u @Operation(summary = "사용자가 낙찰한 경매 목록 조회", description = "사용자가 낙찰한 경매 목록을 조회합니다.") @GetMapping("/users/won") - ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "사용자가 낙찰실패한 경매 목록 조회", description = "사용자가 낙찰실패한 경매 목록을 조회합니다.") @GetMapping("/users/lost") - ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "사용자가 좋아요(찜)한 경매 목록 조회", description = "사용자가 좋아요(찜)한 경매 목록을 조회합니다.") @GetMapping("/users/likes") ResponseEntity> getLikedAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); @Operation(summary = "경매 등록", description = "경매를 등록합니다.") @ApiResponseExplanations( @@ -108,18 +112,10 @@ ResponseEntity> getLikedAuctionList(@LoginUser Long use } ) @PostMapping - ResponseEntity registerAuction(@LoginUser - Long userId, - - @RequestPart("request") - @Valid - RegisterRequest request, - - @RequestPart(value = "images") - @Valid - @NotEmptyMultipartList - @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") - List images); + ResponseEntity registerAuction(@LoginUser Long userId, + @RequestPart("request") @Valid RegisterRequest request, + @RequestPart(value = "images") @Valid + @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images); @Operation(summary = "경매 테스트 등록", description = "테스트 등록합니다.") @PostMapping("/test") diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java index 5881f365..6206223b 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java @@ -10,7 +10,11 @@ import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; +import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.auctionv2.service.AuctionCategoryService; @@ -68,18 +72,18 @@ public ResponseEntity> getCategoryList() { */ @Override @GetMapping("/users/proceeding") - public ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + public ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserProceedingAuctionList(userId, pageable)); } /** * 사용자가 등록한 종료된 경매 목록 조회 */ @Override - public ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + public ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserEndedAuctionList(userId, pageable)); } /** @@ -96,18 +100,18 @@ public ResponseEntity> getUserPreAuctionList(@LoginUser * 사용자가 낙찰한 경매 목록 조회 */ @Override - public ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + public ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserWonAuctionList(userId, pageable)); } /** * 사용자가 낙찰실패한 경매 목록 조회 */ @Override - public ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return null; + public ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest-v2") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserLostAuctionList(userId, pageable)); } /** @@ -125,18 +129,10 @@ public ResponseEntity> getLikedAuctionList(@LoginUser L */ @Override @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) - public ResponseEntity registerAuction(@LoginUser - Long userId, - - @RequestPart("request") - @Valid - RegisterRequest request, - - @RequestPart(value = "images") - @Valid - @NotEmptyMultipartList - @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") - List images) { + public ResponseEntity registerAuction(@LoginUser Long userId, + @RequestPart("request") @Valid RegisterRequest request, + @RequestPart(value = "images") @Valid + @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images) { AuctionRegisterType type = request.auctionRegisterType(); type.getService().register(userId, request, images);//요청 타입에 따라 다른 서비스 호출 return ResponseEntity.status(HttpStatus.CREATED).build(); diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java new file mode 100644 index 00000000..d903427f --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java @@ -0,0 +1,26 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EndedAuctionResponse extends BaseAuctionResponse { + private Long participantCount; + private Long winningBidAmount; + private Boolean isWon; + private Boolean isOrdered; + private LocalDateTime createAt; + + public EndedAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long participantCount, Long winningBidAmount, Boolean isWon, Boolean isOrdered, + LocalDateTime createAt) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.participantCount = participantCount; + this.winningBidAmount = winningBidAmount; + this.isWon = isWon; + this.isOrdered = isOrdered; + this.createAt = createAt; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java new file mode 100644 index 00000000..ff7d8c7f --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java @@ -0,0 +1,21 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class LostAuctionResponse extends BaseAuctionResponse { + private Long participantCount; + private LocalDateTime endDateTime; + private Long bidAmount; + + public LostAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long participantCount, LocalDateTime endDateTime, Long bidAmount) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.participantCount = participantCount; + this.endDateTime = endDateTime; + this.bidAmount = bidAmount; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java new file mode 100644 index 00000000..53eb9cb2 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java @@ -0,0 +1,26 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.domain.auctionv2.entity.AuctionStatus; + +@Getter +@NoArgsConstructor +public class ProceedingAuctionResponse extends BaseAuctionResponse { + private Long timeRemaining; + private AuctionStatus status; + private Long participantCount; + private LocalDateTime createdAt; + + public ProceedingAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, + Boolean isSeller, + Long timeRemaining, AuctionStatus status, Long participantCount, + LocalDateTime createdAt) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.timeRemaining = timeRemaining; + this.status = status; + this.participantCount = participantCount; + this.createdAt = createdAt; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java new file mode 100644 index 00000000..181199fc --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java @@ -0,0 +1,26 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class WonAuctionResponse extends BaseAuctionResponse { + private Long participantCount; + private LocalDateTime endDateTime; + private Long winningAmount; + private Boolean isOrdered; + private Long orderId; + + public WonAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long participantCount, LocalDateTime endDateTime, Long winningAmount, Boolean isOrdered, + Long orderId) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.participantCount = participantCount; + this.endDateTime = endDateTime; + this.winningAmount = winningAmount; + this.isOrdered = isOrdered; + this.orderId = orderId; + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java index 3e9f6acc..2622dd7f 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java @@ -3,7 +3,9 @@ import static com.querydsl.core.types.dsl.Expressions.numberTemplate; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.ENDED; import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PRE; +import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PROCEEDING; import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; @@ -19,6 +21,8 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; @@ -29,12 +33,16 @@ import lombok.RequiredArgsConstructor; import org.chzz.market.common.util.QuerydslOrder; import org.chzz.market.common.util.QuerydslOrderProvider; +import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.QWonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.Category; import org.chzz.market.domain.bid.entity.QBid; @@ -281,6 +289,148 @@ public Page findLikedAuctionsByUserId(Long userId, Pageable return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } + /** + * 사용자가 등록한 진행 중인 경매 목록 조회 + */ + public Page findProceedingAuctionsByUserId(Long userId, Pageable pageable) { + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .join(auctionV2.seller, user).on(user.id.eq(userId)) + .where(auctionV2.status.eq(PROCEEDING)); + + List content = baseQuery + .select( + Projections.constructor( + ProceedingAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + timeRemaining().longValue(), + auctionV2.status, + auctionV2.bidCount, + auctionV2.createdAt + ) + ) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery.select(auctionV2.count()); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + /** + * 사용자가 등록한 종료된 경매 목록 조회 + */ + public Page findEndedAuctionsByUserId(Long userId, Pageable pageable) { + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .join(auctionV2.seller, user).on(user.id.eq(userId)) + .where(auctionV2.status.eq(ENDED)); + + List content = baseQuery + .select( + Projections.constructor( + EndedAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + auctionV2.bidCount, + getWinningBidAmount(), + auctionV2.winnerId.isNotNull(), + orderV2.isNotNull(), + auctionV2.createdAt + ) + ) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .leftJoin(orderV2).on(orderV2.auction.eq(auctionV2)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery.select(auctionV2.count()); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + /** + * 사용자가 낙찰한 경매 목록 조회 + */ + public Page findWonAuctionsByUserId(Long userId, Pageable pageable) { + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .join(bid).on(bid.auctionId.eq(auctionV2.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) + .where(auctionV2.winnerId.eq(userId).and(auctionV2.status.eq(ENDED))); + + List content = baseQuery + .select( + Projections.constructor( + WonAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + auctionV2.bidCount, + auctionV2.endDateTime, + bid.amount, + orderV2.isNotNull(), + orderV2.id + ) + ) + .join(auctionV2.seller, user) + .leftJoin(orderV2).on(orderV2.auction.id.eq(auctionV2.id)) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery.select(auctionV2.count()); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + + /** + * 사용자가 낙찰 실패한 경매 목록 조회 + */ + public Page findLostAuctionsByUserId(Long userId, Pageable pageable) { + JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) + .join(bid).on(bid.auctionId.eq(auctionV2.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) + .where(auctionV2.winnerId.ne(userId).and(auctionV2.status.eq(ENDED))); + + List content = baseQuery + .select( + Projections.constructor( + LostAuctionResponse.class, + auctionV2.id, + auctionV2.name, + imageV2.cdnPath, + auctionV2.minPrice.longValue(), + userIdEq(userId), + auctionV2.bidCount, + auctionV2.endDateTime, + bid.amount + ) + ) + .join(auctionV2.seller, user) + .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = baseQuery.select(auctionV2.count()); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + private List getImagesByAuctionId(Long auctionId) { return jpaQueryFactory .select(new QImageResponse(imageV2.id, imageV2.cdnPath)) @@ -323,6 +473,15 @@ private static NumberExpression timeRemaining() { "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 } + private JPQLQuery getWinningBidAmount() { + return JPAExpressions.select(bid.amount.max().coalesce(0L)) + .from(bid) + .where( + bid.auctionId.eq(auctionV2.id), + bid.status.eq(ACTIVE) + ); + } + @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum AuctionOrder implements QuerydslOrder { diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java index e3cc3665..9a5a75b9 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java @@ -1,7 +1,11 @@ package org.chzz.market.domain.auctionv2.service; import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -28,4 +32,31 @@ public Page getLikedAuctionList(Long userId, Pageable pageab return auctionQueryRepository.findLikedAuctionsByUserId(userId, pageable); } + /** + * 사용자가 등록한 진행 중인 경매 목록 조회 + */ + public Page getUserProceedingAuctionList(Long userId, Pageable pageable) { + return auctionQueryRepository.findProceedingAuctionsByUserId(userId, pageable); + } + + /** + * 사용자가 등록한 종료된 경매 목록 조회 + */ + public Page getUserEndedAuctionList(Long userId, Pageable pageable) { + return auctionQueryRepository.findEndedAuctionsByUserId(userId, pageable); + } + + /** + * 사용자가 낙찰 성공한 경매 목록 조회 + */ + public Page getUserWonAuctionList(Long userId, Pageable pageable) { + return auctionQueryRepository.findWonAuctionsByUserId(userId, pageable); + } + + /** + * 사용자가 낙찰 실패한 경매 목록 조회 + */ + public Page getUserLostAuctionList(Long userId, Pageable pageable) { + return auctionQueryRepository.findLostAuctionsByUserId(userId, pageable); + } } diff --git a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java index 03b8b5cf..fa148a73 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java @@ -5,10 +5,14 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; import org.chzz.market.domain.auctionv2.entity.AuctionStatus; import org.chzz.market.domain.auctionv2.entity.AuctionV2; import org.chzz.market.domain.auctionv2.entity.Category; @@ -52,17 +56,17 @@ class AuctionV2QueryRepositoryTest { private LikeV2Repository likeV2Repository; private User seller; - private User user; + private User user, user1; private ImageV2 defaultImage; @BeforeEach void setUp() { seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); user = User.builder().email("user").providerId("user").providerType(User.ProviderType.KAKAO).build(); + user1 = User.builder().email("user1").providerId("user1").providerType(User.ProviderType.KAKAO).build(); defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); - userRepository.save(seller); - userRepository.save(user); + userRepository.saveAll(List.of(seller, user, user1)); } private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId, @@ -495,5 +499,143 @@ class MyAuctions { assertThat(response2.getIsSeller()).isTrue(); assertThat(response2.getIsLiked()).isFalse(); } + + @Test + void 사용자가_등록한_진행중인_경매_목록_조회() { + // Given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, + 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, + 2000); + AuctionV2 auction3 = createAuction(user, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.PROCEEDING, null, 1500); + AuctionV2 auction4 = createAuction(seller, "종료 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, null, 2000); + AuctionV2 auction5 = createAuction(seller, "사전 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionV2Repository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5)); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + // When + Page result = auctionQueryRepository.findProceedingAuctionsByUserId( + seller.getId(), pageable); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + + ProceedingAuctionResponse response1 = result.getContent().get(0); + assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getIsSeller()).isTrue(); + + ProceedingAuctionResponse response2 = result.getContent().get(1); + assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getIsSeller()).isTrue(); + } + + @Test + void 사용자가_등록한_종료된_경매_목록_조회() { + // Given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, null, 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user.getId(), + 2000); + AuctionV2 auction3 = createAuction(user, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.ENDED, null, 1500); + AuctionV2 auction4 = createAuction(seller, "진행중 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, + 2000); + AuctionV2 auction5 = createAuction(seller, "사전 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionV2Repository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5)); + + createOrder(auction2, user, 2000L); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + // When + Page result = auctionQueryRepository.findEndedAuctionsByUserId( + seller.getId(), pageable); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + + EndedAuctionResponse response1 = result.getContent().get(0); + assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getIsSeller()).isTrue(); + assertThat(response1.getIsWon()).isTrue(); + assertThat(response1.getIsOrdered()).isTrue(); + + EndedAuctionResponse response2 = result.getContent().get(1); + assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getIsSeller()).isTrue(); + assertThat(response2.getIsWon()).isFalse(); + assertThat(response2.getIsOrdered()).isFalse(); + } + + @Test + void 사용자가_낙찰한_경매_목록_조회() { + // given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, user.getId(), + 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user.getId(), + 2000); + AuctionV2 auction3 = createAuction(seller, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.ENDED, null, 1500); + auctionV2Repository.saveAll(List.of(auction1, auction2, auction3)); + + // 사용자의 입찰 생성 + createBid(user, auction1, 2000L, Bid.BidStatus.ACTIVE); + createBid(user, auction2, 3000L, Bid.BidStatus.ACTIVE); + createOrder(auction1, user, 2000L); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + // When + Page result = auctionQueryRepository.findWonAuctionsByUserId( + user.getId(), pageable); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + + WonAuctionResponse response1 = result.getContent().get(0); + assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getIsOrdered()).isFalse(); + assertThat(response1.getWinningAmount()).isEqualTo(3000L); + + WonAuctionResponse response2 = result.getContent().get(1); + assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getIsOrdered()).isTrue(); + assertThat(response2.getWinningAmount()).isEqualTo(2000L); + } + + @Test + void 사용자가_낙찰_실패한_경매_목록_조회() { + // given + AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, user1.getId(), + 1000); + AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user1.getId(), + 2000); + auctionV2Repository.saveAll(List.of(auction1, auction2)); + + // 사용자의 입찰 생성 (하지만 낙찰되지 않음) + createBid(user, auction1, 1000L, Bid.BidStatus.ACTIVE); + createBid(user1, auction1, 2000L, Bid.BidStatus.ACTIVE); + createBid(user, auction2, 2000L, Bid.BidStatus.ACTIVE); + createBid(user1, auction2, 3000L, Bid.BidStatus.ACTIVE); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + + // when + Page result = auctionQueryRepository.findLostAuctionsByUserId( + user.getId(), pageable); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + + LostAuctionResponse response1 = result.getContent().get(0); + assertThat(response1.getProductName()).isEqualTo("아이패드"); + + LostAuctionResponse response2 = result.getContent().get(1); + assertThat(response2.getProductName()).isEqualTo("맥북프로"); + } + + } } From 68f9ae1c666f1687d4291f532fd3c50c4c17baea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=98=88=EC=B0=AC?= <88381563+YeaChan05@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:48:19 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=EC=A0=84=ED=99=98=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 경매 수정 엔드포인트 구현 경매 수정 엔드포인트 구현 * feat: 경매 수정 서비스 구현 경매 업데이트 쿼리 및 이미지 수정 이벤트 구현 * feat: 이미지 업데이트 이벤트 구현 이미지 업데이트 이벤트 구현 * feat: 경매 도메인 로직 구현 경매 검증, 업데이트, 이미지 제거 로직 구현 * feat: 경매 요청, 응답 객체 구현 기존 객체와 유사한 요청 및 응답 객체 구현 * feat: 이미지 업로드 이벤트 핸들러 구현 이미지 순서쌍을 통한 이미지 업로드 구현 * feat: 이미지 업로드 응답 객체 구현 이미지 업로드 응답 객체 구현 * docs: 경매 수정 spring docs 작성 경매 수정 spring docs 작성 * refactor: 메서드명 수정 `updateProduct` -> `updateAuction` * refactor: 도메인 검증 메서드 적용 서비스절 검증이 아닌 도메인 검증 메서드 사용 * refactor: 경매 수정 예외 구체화 사전 경매가 아닌 경매를 수정 시도시 발생하는 예외 구체화 * style: 로그 구체화 로그를 `Product` -> `Auction`으로 수정 * fix: 이미지 경로 수정 이미지 경로 앞에 cdn 경로 명시 --- .../controller/AuctionDetailApi.java | 22 ++- .../controller/AuctionDetailController.java | 14 +- .../dto/AuctionImageUpdateEvent.java | 11 ++ .../dto/request/UpdateAuctionRequest.java | 40 +++++ .../auctionv2/dto/response/ImageResponse.java | 12 ++ .../dto/response/UpdateAuctionResponse.java | 28 ++++ .../domain/auctionv2/entity/AuctionV2.java | 22 +++ .../auctionv2/error/AuctionErrorCode.java | 8 + .../service/AuctionModifyService.java | 60 +++++++ .../imagev2/service/ImageV2Service.java | 147 ++++++++++++++++-- 10 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java create mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java index 78225e84..50eec8fb 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java @@ -4,7 +4,11 @@ import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ALREADY_OFFICIAL; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_ENDED; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.INVALID_IMAGE_COUNT; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.MAX_IMAGE_COUNT_EXCEEDED; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NOT_A_PRE_AUCTION; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NOW_WINNER; +import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NO_IMAGES_PROVIDED; import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.OFFICIAL_AUCTION_DELETE_FORBIDDEN; import static org.chzz.market.domain.imagev2.error.ImageErrorCode.Const.IMAGE_DELETE_FAILED; @@ -19,14 +23,14 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; +import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; +import org.chzz.market.domain.auctionv2.dto.response.UpdateAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.imagev2.error.ImageErrorCode; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.dto.UpdateProductResponse; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -92,9 +96,19 @@ ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 수정", description = "특정 경매를 수정합니다.") - ResponseEntity updateAuction(@LoginUser Long userId, + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_ACCESS_FORBIDDEN, name = "경매 수정 권한이 없는 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = MAX_IMAGE_COUNT_EXCEEDED, name = "이미지가 5장 이상인 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = INVALID_IMAGE_COUNT, name = "이미지 수량이 1개 미만인 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = NO_IMAGES_PROVIDED, name = "업로드 이후 이미지 갯수에 문제가 발생한 경우"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = NOT_A_PRE_AUCTION, name = "사전 경매가 아닌 경매를 업데이트 시도하는 경우"), + } + ) + ResponseEntity updateAuction(@LoginUser Long userId, @PathVariable Long auctionId, - @RequestPart @Valid UpdateProductRequest request, + @RequestPart @Valid UpdateAuctionRequest request, @RequestParam(required = false) Map images); @Operation(summary = "특정 경매 삭제", description = "특정 경매를 삭제합니다. 삭제는 사전경매만 가능합니다.") diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java index ae65f933..3da14a19 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java @@ -5,16 +5,17 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; +import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auctionv2.dto.response.UpdateAuctionResponse; import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; import org.chzz.market.domain.auctionv2.service.AuctionDeleteService; import org.chzz.market.domain.auctionv2.service.AuctionDetailService; +import org.chzz.market.domain.auctionv2.service.AuctionModifyService; import org.chzz.market.domain.auctionv2.service.AuctionStartService; import org.chzz.market.domain.auctionv2.service.AuctionWonService; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.bid.service.BidLookupService; import org.chzz.market.domain.likev2.service.LikeUpdateService; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.dto.UpdateProductResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -35,6 +36,7 @@ public class AuctionDetailController implements AuctionDetailApi { private final AuctionDetailService auctionDetailService; private final AuctionDeleteService auctionDeleteService; + private final AuctionModifyService auctionModifyService; private final AuctionStartService auctionStartService; private final AuctionWonService auctionWonService; private final BidLookupService bidLookupService; @@ -79,10 +81,12 @@ public ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Lo @Override @PatchMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity updateAuction(Long userId, Long auctionId, - UpdateProductRequest request, + public ResponseEntity updateAuction(Long userId,@PathVariable Long auctionId, + UpdateAuctionRequest request, Map images) { - return null; + UpdateAuctionResponse response = + auctionModifyService.updateAuction(userId, auctionId, request, images); + return ResponseEntity.ok(response); } @Override diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java new file mode 100644 index 00000000..5561a80d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java @@ -0,0 +1,11 @@ +package org.chzz.market.domain.auctionv2.dto; + +import java.util.Map; +import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.springframework.web.multipart.MultipartFile; + +public record AuctionImageUpdateEvent(AuctionV2 auction, + UpdateAuctionRequest request, + Map imageBuffer) { +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java new file mode 100644 index 00000000..7c4a4a48 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java @@ -0,0 +1,40 @@ +package org.chzz.market.domain.auctionv2.dto.request; + +import static org.chzz.market.domain.auction.dto.request.BaseRegisterRequest.DESCRIPTION_REGEX; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.chzz.market.common.validation.annotation.ThousandMultiple; +import org.chzz.market.domain.auctionv2.entity.Category; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdateAuctionRequest { + @Size(min = 2, max = 30, message = "제목은 최소 2글자 이상 30자 이하여야 합니다") + private String productName; + + @Schema(description = "개행문자 포함 최대 1000자, 개행문자 최대 10개") + @Size(max = 1000, message = "상품설명은 1000자 이내여야 합니다.") + @Pattern(regexp = DESCRIPTION_REGEX, message = "줄 바꿈 10번까지 가능합니다") + protected String description; + + private Category category; + + @ThousandMultiple + @Max(value = 2_000_000, message = "최소금액은 200만원을 넘을 수 없습니다") + private Integer minPrice; + + @Builder.Default + private Map imageSequence = new HashMap<>(); +} + diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java new file mode 100644 index 00000000..8ef89bc5 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java @@ -0,0 +1,12 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import org.chzz.market.domain.image.entity.ImageV2; + +public record ImageResponse( + Long imageId, + String imageUrl +) { + public static ImageResponse from(ImageV2 imageV2) { + return new ImageResponse(imageV2.getId(), imageV2.getCdnPath()); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java new file mode 100644 index 00000000..7706de6c --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java @@ -0,0 +1,28 @@ +package org.chzz.market.domain.auctionv2.dto.response; + +import java.util.List; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.entity.Category; + +public record UpdateAuctionResponse( + Long auctionId, + String auctionName, + String description, + Category category, + Integer minPrice, + List imageUrls +) { + public static UpdateAuctionResponse from(AuctionV2 auction) { + return new UpdateAuctionResponse( + auction.getId(), + auction.getName(), + auction.getDescription(), + auction.getCategory(), + auction.getMinPrice(), + auction.getImages() + .stream() + .map(ImageResponse::from) + .toList() + ); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java index 92af397c..18f02d88 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java @@ -30,6 +30,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.base.entity.BaseTimeEntity; import org.chzz.market.domain.image.entity.ImageV2; @@ -167,4 +169,24 @@ public void endAuction() { public void assignWinner(final Long bidderId) { this.winnerId = bidderId; } + + public void update(final UpdateAuctionRequest request) { + this.name = request.getProductName(); + this.description = request.getDescription(); + this.category = request.getCategory(); + this.minPrice = request.getMinPrice(); + } + + public void validateImageSize() { + int count = this.images.size(); + if (count < 1) { + throw new AuctionException(AuctionErrorCode.NO_IMAGES_PROVIDED); + } else if (count > 5) { + throw new AuctionException(AuctionErrorCode.MAX_IMAGE_COUNT_EXCEEDED); + } + } + + public void removeImages(final List imagesToRemove) { + this.images.removeAll(imagesToRemove); + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java index a5691bf5..7b0f1f20 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java +++ b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java @@ -17,6 +17,10 @@ public enum AuctionErrorCode implements ErrorCode { AUCTION_ENDED(BAD_REQUEST, "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY(BAD_REQUEST, "진행중인 경매 목록 조회 시에만 minutes 파라미터를 사용할 수 있습니다."), + INVALID_IMAGE_COUNT(HttpStatus.BAD_REQUEST, "이미지 개수가 올바르지 않습니다."), + MAX_IMAGE_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지는 최대 5개까지 등록할 수 있습니다."), + NOT_A_PRE_AUCTION(BAD_REQUEST, "사전 등록 경매가 아닙니다"), + NO_IMAGES_PROVIDED(HttpStatus.BAD_REQUEST, "이미지가 제공되지 않았습니다."), OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), NOW_WINNER(FORBIDDEN, "낙찰자가 아닙니다."), AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), @@ -30,7 +34,11 @@ public static class Const { public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; public static final String AUCTION_ENDED = "AUCTION_ENDED"; public static final String END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY = "END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY"; + public static final String INVALID_IMAGE_COUNT = "INVALID_IMAGE_COUNT"; public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; + public static final String MAX_IMAGE_COUNT_EXCEEDED = "MAX_IMAGE_COUNT_EXCEEDED"; + public static final String NOT_A_PRE_AUCTION = "NOT_A_PRE_AUCTION"; + public static final String NO_IMAGES_PROVIDED = "NO_IMAGES_PROVIDED"; public static final String NOW_WINNER = "NOW_WINNER"; public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; public static final String AUCTION_NOT_FOUND = "AUCTION_NOT_FOUND"; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java new file mode 100644 index 00000000..73d55f0a --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java @@ -0,0 +1,60 @@ +package org.chzz.market.domain.auctionv2.service; + +import java.util.Collections; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.dto.AuctionImageUpdateEvent; +import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auctionv2.dto.response.UpdateAuctionResponse; +import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; +import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuctionModifyService { + private final AuctionV2Repository auctionV2Repository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public UpdateAuctionResponse updateAuction(Long userId, Long auctionId, + UpdateAuctionRequest request, + Map newImages) { + // 경매 조회 + AuctionV2 auction = auctionV2Repository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AuctionErrorCode.AUCTION_NOT_FOUND)); + + // 사용자 권한 체크 + auction.validateOwner(userId); + + // 경매 등록 상태 유무 유효성 검사 + if(!auction.isPreAuction()){ + throw new AuctionException(AuctionErrorCode.NOT_A_PRE_AUCTION); + } + + // 경매 정보 업데이트 + auction.update(request); + + // 이미지 업데이트 이벤트 + Map imageBuffer = removeRequestKey(newImages);//request 제거 + AuctionImageUpdateEvent event = new AuctionImageUpdateEvent(auction, request, imageBuffer); + eventPublisher.publishEvent(event); + + log.info("경매 ID {}번에 대한 사전 등록 정보를 업데이트를 완료했습니다.", auctionId); + return UpdateAuctionResponse.from(auction); + } + + private Map removeRequestKey(Map newImages) { + if (newImages != null) { + newImages.remove("request"); + } + return newImages != null ? newImages : Collections.emptyMap(); + } +} diff --git a/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java b/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java index 258c1de8..a697ce37 100644 --- a/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java +++ b/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java @@ -2,29 +2,43 @@ import static org.chzz.market.domain.image.error.ImageErrorCode.INVALID_IMAGE_EXTENSION; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auctionv2.dto.AuctionImageUpdateEvent; import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; +import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; import org.chzz.market.domain.auctionv2.entity.AuctionV2; +import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; +import org.chzz.market.domain.auctionv2.error.AuctionException; import org.chzz.market.domain.image.entity.ImageV2; import org.chzz.market.domain.image.error.exception.ImageException; import org.chzz.market.domain.image.service.S3ImageUploader; import org.chzz.market.domain.imagev2.repository.ImageV2Repository; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +@Slf4j @Service @RequiredArgsConstructor public class ImageV2Service { private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); + @Value("${cloud.aws.cloudfront.domain}") + private String cloudfrontDomain; + private final ImageV2Repository imageRepository; private final S3ImageUploader s3ImageUploader; @@ -36,34 +50,55 @@ public void uploadImages(final ImageUploadEvent event) { AuctionV2 auction = event.auction(); - List list = paths.stream() - .map(path -> createImage(path, auction)).toList(); + List list = createImages(auction, paths); auction.addImages(list); imageRepository.saveAll(list); } + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void modifyImages(AuctionImageUpdateEvent event) { + AuctionV2 auction = event.auction(); + UpdateAuctionRequest request = event.request(); + Map buffer = event.imageBuffer(); + Map sequence = Optional.ofNullable(request.getImageSequence()).orElse(Collections.emptyMap()); + updateAuctionImages(auction, sequence, buffer); + } + + /** + * key - unique한 이미지 파일명
value - 해당 파일의 {@link MultipartFile} + */ private Map setImageBuffer(final ImageUploadEvent event) { Map imageBuffer = new HashMap<>(); for (MultipartFile image : event.images()) { - String originalFilename = image.getOriginalFilename(); - String uniqueFileName = createUniqueFileName(originalFilename); + String uniqueFileName = createUniqueFileName(image); imageBuffer.put(uniqueFileName, image); } return imageBuffer; } - private ImageV2 createImage(final String path, final AuctionV2 auction) { - return ImageV2.builder() - .auction(auction) - .cdnPath(path) - .build(); + /** + * @param paths 업로드된 이미지의 cdn 경로들 + * @return cdn과 순서를 적용한 {@link ImageV2} list + */ + private List createImages(final AuctionV2 auction, final List paths) { + return IntStream.range(0, paths.size()) + .mapToObj(i -> ImageV2.builder() + .cdnPath(cloudfrontDomain + "/" + paths.get(i)) + .sequence((i + 1)) + .auction(auction) + .build()) + .toList(); } - private String createUniqueFileName(String originalFileName) { + /** + * @param file 업로드한 파일 + * @return 원본파일명을 기반으로한 unique한 파일명 + */ + private String createUniqueFileName(MultipartFile file) { String uuid = UUID.randomUUID().toString(); - String extension = StringUtils.getFilenameExtension(originalFileName); + String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); if (extension == null || !isValidFileExtension(extension)) { throw new ImageException(INVALID_IMAGE_EXTENSION); @@ -72,7 +107,97 @@ private String createUniqueFileName(String originalFileName) { return uuid + "." + extension; } + /** + * 파일 확장자 검증기
+ * + * @param extension 파일 확장자 + */ private boolean isValidFileExtension(String extension) { return ALLOWED_EXTENSIONS.contains(extension.toLowerCase()); } + + /** + * 이미지 순서쌍을 포함한 변경요청({@link UpdateAuctionRequest})을 이용해 이미지 순서 변경 + */ + private void updateAuctionImages(final AuctionV2 auction, final Map sequence, + final Map multipartFileBuffer) { + validateTotalImageCount(sequence.size() + multipartFileBuffer.size()); + // 기존 이미지 처리 (업데이트할 이미지와 삭제할 이미지 구분) + processExistingImages(auction, sequence); + + // 새 이미지가 있는 경우 + if (!multipartFileBuffer.isEmpty()) { + uploadAndAddNewImages(auction, multipartFileBuffer); + } + auction.validateImageSize();// 업로드 이후 이미지 수량 검증 + + } + + /** + * 요청의 순서 변경 요청과 새로운 이미지 갯수 총합을 통해 크기 검증 + */ + private void validateTotalImageCount(int totalSize) { + if (totalSize > 5) { + throw new AuctionException(AuctionErrorCode.MAX_IMAGE_COUNT_EXCEEDED); + } else if (totalSize == 0) { + throw new AuctionException(AuctionErrorCode.INVALID_IMAGE_COUNT); + } + } + + /** + * 이미지 시퀀스 수정 및 이미지 삭제 + */ + private void processExistingImages(final AuctionV2 auction, final Map sequence) { + List imagesToUpdate = new ArrayList<>(); + List imagesToRemove = new ArrayList<>(); + + auction.getImages().forEach(image -> { + if (sequence.containsKey(image.getId())) { + imagesToUpdate.add(image); // 업데이트할 이미지 + } else { + imagesToRemove.add(image); // 삭제할 이미지 + } + }); + auction.removeImages(imagesToRemove); // 삭제할 이미지 처리 + updateImageSequences(imagesToUpdate, sequence); // 시퀀스 업데이트할 이미지 처리 + } + + /** + * 이미지 순서 업데이트 + */ + private void updateImageSequences(final List imagesToUpdate, final Map sequence) { + imagesToUpdate.forEach(image -> { + Long imageId = image.getId(); + Integer newSequence = sequence.get(imageId); + if (newSequence != null) { + image.changeSequence(newSequence); // 이미지의 시퀀스 업데이트 + } + }); + } + + /** + * 이미지 업로드 및 영속화 + */ + private void uploadAndAddNewImages(final AuctionV2 auction, final Map multipartFileBuffer) { + List newImageEntities = uploadSequentialImages(auction, multipartFileBuffer); + auction.addImages(newImageEntities); + log.info("경매 ID {}번의 새 이미지를 성공적으로 저장하였습니다.", auction.getId()); + } + + private List uploadSequentialImages(AuctionV2 auction, Map newImages) { + List images = newImages.entrySet().stream() + .map(entry -> { + int sequence = Integer.parseInt(entry.getKey()); + MultipartFile multipartFile = entry.getValue(); + String uniqueFileName = createUniqueFileName(multipartFile); + String cdnPath = s3ImageUploader.uploadImage(multipartFile, uniqueFileName); + return ImageV2.builder() + .sequence(sequence) + .cdnPath(cloudfrontDomain + "/" + cdnPath) + .auction(auction) + .build(); + }).toList(); + imageRepository.saveAll(images); + return images; + } } From 516e35dce4bcf845e5750f06a59d1f371c394261 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:09:02 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20API=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=99=84=EB=A3=8C=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 이미지 응답 dto 패키지 변경 * refactor: API 전환 리팩토링 * refactor: 유저 프로필 조회 API 전환 * refactor: v2 -> v1 버전 수정 * docs: api docs 수정 * test: 버전 변경으로 인한 테스트 코드 수정 * docs: 문서화 수정 --- .../domain/auction/controller/AuctionApi.java | 164 ++-- .../auction/controller/AuctionController.java | 202 ++--- .../controller/AuctionDetailApi.java | 48 +- .../controller/AuctionDetailController.java | 24 +- .../auction/dto/AuctionImageUpdateEvent.java | 11 + .../dto/AuctionRegisterType.java | 8 +- .../dto/AuctionRegistrationEvent.java | 2 +- .../domain/auction/dto/BaseAuctionDto.java | 22 - .../domain/auction/dto/ImageUploadEvent.java | 8 + .../dto/request/BaseRegisterRequest.java | 51 -- .../dto/request/PreRegisterRequest.java | 11 - .../dto/request/RegisterAuctionRequest.java | 11 - .../dto/request/RegisterRequest.java | 6 +- .../dto/request/StartAuctionRequest.java | 16 - .../dto/request/UpdateAuctionRequest.java | 8 +- .../dto/response/AuctionDetailsResponse.java | 83 -- .../AuctionParticipationResponse.java | 13 - .../auction/dto/response/AuctionResponse.java | 32 - .../response/BaseAuctionDetailResponse.java | 8 +- .../dto/response/BaseAuctionResponse.java | 2 +- .../dto/response/CategoryResponse.java | 2 +- .../dto/response/EndedAuctionResponse.java | 2 +- .../dto/response/LostAuctionResponse.java | 29 +- .../OfficialAuctionDetailResponse.java | 6 +- .../dto/response/OfficialAuctionResponse.java | 2 +- .../response/PreAuctionDetailResponse.java | 6 +- .../dto/response/PreAuctionResponse.java | 2 +- .../dto/response/PreRegisterResponse.java | 24 - .../response/ProceedingAuctionResponse.java | 4 +- .../dto/response/RegisterAuctionResponse.java | 24 - .../dto/response/RegisterResponse.java | 6 - .../dto/response/SimpleAuctionResponse.java | 13 - .../dto/response/StartAuctionResponse.java | 22 - .../dto/response/UpdateAuctionResponse.java | 11 +- .../dto/response/UserAuctionResponse.java | 29 - .../response/UserEndedAuctionResponse.java | 24 - .../response/WonAuctionDetailsResponse.java | 3 +- .../dto/response/WonAuctionResponse.java | 36 +- .../market/domain/auction/entity/Auction.java | 169 +++- .../entity/AuctionStatus.java | 2 +- .../entity/Category.java | 2 +- .../listener/AuctionEntityListener.java | 46 - .../auction/error/AuctionErrorCode.java | 35 +- .../repository/AuctionQueryRepository.java} | 396 ++++---- .../auction/repository/AuctionRepository.java | 33 +- .../repository/AuctionRepositoryCustom.java | 122 --- .../AuctionRepositoryCustomImpl.java | 609 ------------- .../auction/schedule/AuctionEndJob.java | 13 +- .../service/AuctionCategoryService.java | 6 +- .../service/AuctionDeleteService.java | 24 +- .../service/AuctionDetailService.java | 18 +- .../service/AuctionEndService.java | 18 +- .../service/AuctionLookupService.java | 14 +- .../service/AuctionModifyService.java | 22 +- .../service/AuctionMyService.java | 16 +- .../service/AuctionRegistrationService.java | 22 +- .../AuctionRegistrationServiceFactory.java | 26 - .../service/AuctionSchedulingService.java | 8 +- .../auction/service/AuctionService.java | 276 ------ .../service/AuctionStartService.java | 22 +- .../AuctionTestService.java} | 36 +- .../auction/service/AuctionWonService.java | 36 + .../PreAuctionRegistrationService.java | 20 +- .../service/RegistrationService.java | 4 +- .../register/AuctionRegisterService.java | 69 -- .../register/AuctionRegistrationService.java | 11 - .../service/register/PreRegisterService.java | 51 -- .../auction/type/AuctionRegisterType.java | 13 - .../domain/auction/type/AuctionStatus.java | 13 - .../domain/auction/type/AuctionViewType.java | 6 - .../auctionv2/controller/AuctionV2Api.java | 124 --- .../controller/AuctionV2Controller.java | 151 ---- .../dto/AuctionImageUpdateEvent.java | 11 - .../auctionv2/dto/ImageUploadEvent.java | 8 - .../auctionv2/dto/response/ImageResponse.java | 12 - .../dto/response/LostAuctionResponse.java | 21 - .../response/WonAuctionDetailsResponse.java | 14 - .../dto/response/WonAuctionResponse.java | 26 - .../domain/auctionv2/entity/AuctionV2.java | 192 ---- .../auctionv2/error/AuctionErrorCode.java | 46 - .../auctionv2/error/AuctionException.java | 10 - .../repository/AuctionV2Repository.java | 30 - .../auctionv2/schedule/AuctionV2EndJob.java | 22 - .../auctionv2/service/AuctionTestService.java | 66 -- .../auctionv2/service/AuctionWonService.java | 36 - .../market/domain/bid/controller/BidApi.java | 12 +- .../domain/bid/controller/BidController.java | 6 +- .../domain/bid/dto/query/BiddingRecord.java | 18 - .../dto/{ => request}/BidCreateRequest.java | 2 +- .../bid/dto/response/BiddingRecord.java | 20 + .../chzz/market/domain/bid/entity/Bid.java | 3 - .../bid/repository/BidQueryRepository.java | 64 +- .../domain/bid/repository/BidRepository.java | 2 +- .../bid/repository/BidRepositoryCustom.java | 18 - .../repository/BidRepositoryCustomImpl.java | 146 --- .../bid/service/BidCancelLockService.java | 4 +- .../domain/bid/service/BidCreateService.java | 16 +- .../domain/bid/service/BidLookupService.java | 20 +- .../market/domain/bid/service/BidService.java | 116 --- .../dto/{ => response}/ImageResponse.java | 3 +- .../market/domain/image/entity/Image.java | 18 +- .../domain/image/error/ImageErrorCode.java | 19 +- .../service/ImageDeleteService.java | 12 +- .../domain/image/service/ImageService.java | 227 +++-- .../domain/image/service/ImageUploader.java | 11 - .../domain/image/service/S3ImageUploader.java | 25 +- .../market/domain/imagev2/entity/ImageV2.java | 53 -- .../domain/imagev2/error/ImageErrorCode.java | 21 - .../error/exception/ImageException.java | 10 - .../imagev2/repository/ImageV2Repository.java | 7 - .../imagev2/service/ImageV2Service.java | 203 ----- .../market/domain/like/dto/LikeResponse.java | 7 - .../chzz/market/domain/like/entity/Like.java | 27 +- .../domain/like/error/LikeErrorCode.java | 19 - .../domain/like/error/LikeException.java | 10 - .../like/repository/LikeRepository.java | 13 +- .../domain/like/service/LikeService.java | 95 -- .../service/LikeUpdateService.java | 22 +- .../market/domain/likev2/entity/LikeV2.java | 38 - .../likev2/repository/LikeV2Repository.java | 12 - .../market/domain/orderv2/entity/OrderV2.java | 74 -- .../orderv2/repository/OrderV2Repository.java | 7 - .../domain/paymentv2/entity/PaymentV2.java | 79 -- .../domain/paymentv2/entity/Status.java | 12 - .../respository/PaymentV2Repository.java | 7 - .../domain/product/controller/ProductApi.java | 60 -- .../product/controller/ProductController.java | 150 ---- .../domain/product/dto/BaseProductDto.java | 20 - .../domain/product/dto/CategoryResponse.java | 3 - .../product/dto/DeleteProductResponse.java | 21 - .../product/dto/ProductDetailsResponse.java | 49 - .../domain/product/dto/ProductResponse.java | 44 - .../product/dto/UpdateProductRequest.java | 39 - .../product/dto/UpdateProductResponse.java | 28 - .../market/domain/product/entity/Product.java | 160 ---- .../product/error/ProductErrorCode.java | 33 - .../product/error/ProductException.java | 10 - .../product/repository/ProductRepository.java | 26 - .../repository/ProductRepositoryCustom.java | 59 -- .../ProductRepositoryCustomImpl.java | 268 ------ .../product/service/ProductService.java | 258 ------ .../domain/user/controller/UserApi.java | 3 - .../user/controller/UserController.java | 13 +- .../dto/response/UserProfileResponse.java | 7 +- .../domain/user/service/UserService.java | 48 +- .../db/migration/V10__modify_field_name.sql | 14 - .../V11__notification_image_path.sql | 21 - .../db/migration/V12__modify_bid_field.sql | 21 - ...elete_notification_user_id_foreign_key.sql | 13 - .../db/migration/V14__create_v2_auction.sql | 26 - .../db/migration/V15__create_like_image.sql | 32 - .../V16__create_v2_order_payment.sql | 55 -- src/main/resources/db/migration/V1__init.sql | 380 ++++---- .../V2__update_default_count_value.sql | 7 - .../db/migration/V3__reorder_columns.sql | 66 -- .../V4__add_profile_image_url_to_users.sql | 8 - .../db/migration/V5__add_image_sequence.sql | 8 - .../db/migration/V6__add_address_columns.sql | 18 - .../db/migration/V7__add_orders_table.sql | 30 - .../db/migration/V8__drop_bank_account.sql | 7 - .../db/migration/V9__drop_users_link.sql | 7 - .../org/chzz/market/common/TestConfig.java | 13 - .../controller/AuctionControllerTest.java | 359 ++------ .../dto/request/BaseRegisterRequestTest.java | 46 - .../entity/AuctionTest.java} | 36 +- .../AuctionQueryRepositoryTest.java} | 204 ++--- .../AuctionRepositoryCustomImplTest.java | 808 ----------------- .../service/AuctionDeleteServiceTest.java | 34 +- .../service/AuctionLookupServiceTest.java | 10 +- .../auction/service/AuctionServiceTest.java | 850 ------------------ .../service/AuctionStartServiceTest.java | 20 +- .../service/AuctionWonServiceTest.java | 28 +- .../controller/AuctionV2ControllerTest.java | 113 --- .../repository/BidQueryRepositoryTest.java | 18 +- .../BidRepositoryCustomImplTest.java | 365 -------- .../bid/service/BidCancelLockServiceTest.java | 26 +- .../BidCreateServiceConcurrencyTest.java | 28 +- .../domain/bid/service/BidServiceTest.java | 324 ------- .../service/ImageDeleteServiceTest.java | 15 +- .../domain/like/service/LikeServiceTest.java | 123 --- .../LikeUpdateServiceConcurrencyTest.java | 30 +- .../ProductRepositoryCustomImplTest.java | 528 ----------- .../product/service/ProductServiceTest.java | 517 ----------- .../domain/user/service/UserServiceTest.java | 46 +- .../chzz/market/util/AuctionTestFactory.java | 30 - .../chzz/market/util/ProductTestFactory.java | 27 - 186 files changed, 1656 insertions(+), 10334 deletions(-) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/controller/AuctionDetailApi.java (78%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/controller/AuctionDetailController.java (83%) create mode 100644 src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/AuctionRegisterType.java (63%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/AuctionRegistrationEvent.java (75%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/BaseAuctionDto.java create mode 100644 src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequest.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/request/PreRegisterRequest.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/request/RegisterAuctionRequest.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/request/RegisterRequest.java (86%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/request/StartAuctionRequest.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/request/UpdateAuctionRequest.java (84%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/AuctionDetailsResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/AuctionParticipationResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/AuctionResponse.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/BaseAuctionDetailResponse.java (84%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/BaseAuctionResponse.java (91%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/CategoryResponse.java (84%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/EndedAuctionResponse.java (94%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/OfficialAuctionDetailResponse.java (92%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/OfficialAuctionResponse.java (92%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/PreAuctionDetailResponse.java (84%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/PreAuctionResponse.java (90%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/PreRegisterResponse.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/ProceedingAuctionResponse.java (88%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/RegisterAuctionResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/RegisterResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/SimpleAuctionResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/StartAuctionResponse.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/dto/response/UpdateAuctionResponse.java (64%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/UserAuctionResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/dto/response/UserEndedAuctionResponse.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/entity/AuctionStatus.java (82%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/entity/Category.java (90%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/entity/listener/AuctionEntityListener.java rename src/main/java/org/chzz/market/domain/{auctionv2/repository/AuctionV2QueryRepository.java => auction/repository/AuctionQueryRepository.java} (50%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustom.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionCategoryService.java (80%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionDeleteService.java (69%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionDetailService.java (65%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionEndService.java (85%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionLookupService.java (69%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionModifyService.java (73%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionMyService.java (77%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionRegistrationService.java (71%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationServiceFactory.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionSchedulingService.java (87%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/service/AuctionService.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionStartService.java (70%) rename src/main/java/org/chzz/market/domain/auction/{type/TestService.java => service/AuctionTestService.java} (71%) create mode 100644 src/main/java/org/chzz/market/domain/auction/service/AuctionWonService.java rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/PreAuctionRegistrationService.java (70%) rename src/main/java/org/chzz/market/domain/{auctionv2 => auction}/service/RegistrationService.java (63%) delete mode 100644 src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegisterService.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegistrationService.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/service/register/PreRegisterService.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/type/AuctionRegisterType.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/type/AuctionStatus.java delete mode 100644 src/main/java/org/chzz/market/domain/auction/type/AuctionViewType.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/error/AuctionException.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java delete mode 100644 src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java delete mode 100644 src/main/java/org/chzz/market/domain/bid/dto/query/BiddingRecord.java rename src/main/java/org/chzz/market/domain/bid/dto/{ => request}/BidCreateRequest.java (94%) create mode 100644 src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java delete mode 100644 src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustom.java delete mode 100644 src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImpl.java delete mode 100644 src/main/java/org/chzz/market/domain/bid/service/BidService.java rename src/main/java/org/chzz/market/domain/image/dto/{ => response}/ImageResponse.java (87%) rename src/main/java/org/chzz/market/domain/{imagev2 => image}/service/ImageDeleteService.java (78%) delete mode 100644 src/main/java/org/chzz/market/domain/image/service/ImageUploader.java delete mode 100644 src/main/java/org/chzz/market/domain/imagev2/entity/ImageV2.java delete mode 100644 src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java delete mode 100644 src/main/java/org/chzz/market/domain/imagev2/error/exception/ImageException.java delete mode 100644 src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java delete mode 100644 src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java delete mode 100644 src/main/java/org/chzz/market/domain/like/dto/LikeResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/like/error/LikeErrorCode.java delete mode 100644 src/main/java/org/chzz/market/domain/like/error/LikeException.java delete mode 100644 src/main/java/org/chzz/market/domain/like/service/LikeService.java rename src/main/java/org/chzz/market/domain/{likev2 => like}/service/LikeUpdateService.java (67%) delete mode 100644 src/main/java/org/chzz/market/domain/likev2/entity/LikeV2.java delete mode 100644 src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java delete mode 100644 src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java delete mode 100644 src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java delete mode 100644 src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java delete mode 100644 src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java delete mode 100644 src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java delete mode 100644 src/main/java/org/chzz/market/domain/product/controller/ProductApi.java delete mode 100644 src/main/java/org/chzz/market/domain/product/controller/ProductController.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/BaseProductDto.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/CategoryResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/DeleteProductResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/ProductDetailsResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/ProductResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/UpdateProductRequest.java delete mode 100644 src/main/java/org/chzz/market/domain/product/dto/UpdateProductResponse.java delete mode 100644 src/main/java/org/chzz/market/domain/product/entity/Product.java delete mode 100644 src/main/java/org/chzz/market/domain/product/error/ProductErrorCode.java delete mode 100644 src/main/java/org/chzz/market/domain/product/error/ProductException.java delete mode 100644 src/main/java/org/chzz/market/domain/product/repository/ProductRepository.java delete mode 100644 src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustom.java delete mode 100644 src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImpl.java delete mode 100644 src/main/java/org/chzz/market/domain/product/service/ProductService.java delete mode 100644 src/main/resources/db/migration/V10__modify_field_name.sql delete mode 100644 src/main/resources/db/migration/V11__notification_image_path.sql delete mode 100644 src/main/resources/db/migration/V12__modify_bid_field.sql delete mode 100644 src/main/resources/db/migration/V13__delete_notification_user_id_foreign_key.sql delete mode 100644 src/main/resources/db/migration/V14__create_v2_auction.sql delete mode 100644 src/main/resources/db/migration/V15__create_like_image.sql delete mode 100644 src/main/resources/db/migration/V16__create_v2_order_payment.sql delete mode 100644 src/main/resources/db/migration/V2__update_default_count_value.sql delete mode 100644 src/main/resources/db/migration/V3__reorder_columns.sql delete mode 100644 src/main/resources/db/migration/V4__add_profile_image_url_to_users.sql delete mode 100644 src/main/resources/db/migration/V5__add_image_sequence.sql delete mode 100644 src/main/resources/db/migration/V6__add_address_columns.sql delete mode 100644 src/main/resources/db/migration/V7__add_orders_table.sql delete mode 100644 src/main/resources/db/migration/V8__drop_bank_account.sql delete mode 100644 src/main/resources/db/migration/V9__drop_users_link.sql delete mode 100644 src/test/java/org/chzz/market/common/TestConfig.java delete mode 100644 src/test/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequestTest.java rename src/test/java/org/chzz/market/domain/{auctionv2/entity/AuctionV2Test.java => auction/entity/AuctionTest.java} (75%) rename src/test/java/org/chzz/market/domain/{auctionv2/repository/AuctionV2QueryRepositoryTest.java => auction/repository/AuctionQueryRepositoryTest.java} (72%) delete mode 100644 src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java rename src/test/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionDeleteServiceTest.java (79%) rename src/test/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionLookupServiceTest.java (70%) delete mode 100644 src/test/java/org/chzz/market/domain/auction/service/AuctionServiceTest.java rename src/test/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionStartServiceTest.java (78%) rename src/test/java/org/chzz/market/domain/{auctionv2 => auction}/service/AuctionWonServiceTest.java (61%) delete mode 100644 src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java delete mode 100644 src/test/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImplTest.java delete mode 100644 src/test/java/org/chzz/market/domain/bid/service/BidServiceTest.java delete mode 100644 src/test/java/org/chzz/market/domain/like/service/LikeServiceTest.java rename src/test/java/org/chzz/market/domain/{likev2 => like}/service/LikeUpdateServiceConcurrencyTest.java (74%) delete mode 100644 src/test/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImplTest.java delete mode 100644 src/test/java/org/chzz/market/domain/product/service/ProductServiceTest.java delete mode 100644 src/test/java/org/chzz/market/util/AuctionTestFactory.java delete mode 100644 src/test/java/org/chzz/market/util/ProductTestFactory.java diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java index 60709139..43bc7efd 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java @@ -1,94 +1,124 @@ package org.chzz.market.domain.auction.controller; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_ALREADY_REGISTERED; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; +import static org.chzz.market.domain.user.error.UserErrorCode.Const.USER_NOT_FOUND; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; import java.util.List; import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.request.StartAuctionRequest; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.AuctionResponse; +import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; +import org.chzz.market.domain.auction.dto.request.RegisterRequest; +import org.chzz.market.domain.auction.dto.response.CategoryResponse; +import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.dto.response.OfficialAuctionResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auction.dto.response.ProceedingAuctionResponse; import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.auction.error.AuctionErrorCode; -import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.product.entity.Product.Category; +import org.chzz.market.domain.user.error.UserErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @Tag(name = "auctions", description = "경매 API") +@RequestMapping("/v1/auctions") public interface AuctionApi { - - @Operation(summary = "경매 목록 조회") - ResponseEntity> getAuctionList(Category category, Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "베스트 경매 목록 조회") - ResponseEntity> bestAuctionList(); - - @Operation(summary = "마감 임박 경매 목록 조회") - ResponseEntity> imminentAuctionList(); - - @Operation(summary = "내가 성공한 경매 목록 조회") - ResponseEntity> getWonAuctionHistory(Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "내가 실패한 경매 조회") - ResponseEntity> getLostAuctionHistory(Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "경매 상세 조회", - description = "주문 여부는 민감한 정보이므로 낙찰자와 판매자인 경우에만 해당 정보를 확인할 수 있습니다. " + - "그 외의 사용자에게는 주문 여부 필드는 응답에 포함되지 않습니다.") - ResponseEntity getAuctionDetails(Long auctionId, Long userId); - - @Operation(summary = "경매 입찰 목록 조회") - ResponseEntity> getBids(Long userId, Long auctionId, @ParameterObject Pageable pageable); - - @Operation(summary = "낙찰 정보 조회") - ResponseEntity getWinningBid(Long userId, Long auctionId); - - @Operation(summary = "내가 등록한 모든 경매 목록 조회(현재 사용 X)") - ResponseEntity> getUserRegisteredAuction(Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "특정 닉네임 사용자의 모든 경매 상품 목록 조회(현재 사용 X)") - ResponseEntity> getUserAuctionList(String nickname, @ParameterObject Pageable pageable); - - @Operation(summary = "내가 등록한 진행 중인 경매 목록 조회") - ResponseEntity> getProceedingAuctions(Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "내가 등록한 종료된 경매 목록 조회") - ResponseEntity> getEndedAuctions(Long userId, Pageable pageable); - - @Operation(summary = "경매 등록") - ResponseEntity registerAuction(Long userId, @Valid BaseRegisterRequest request, - List images); - + @Operation(summary = "경매 목록 조회", description = "경매 목록을 조회합니다. status 파라미터를 통해 조회 유형을 지정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정식 경매 응답(페이징)", + content = {@Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = OfficialAuctionResponse.class)) + )} + ), + @ApiResponse(responseCode = "201", description = "사전 경매 응답(페이징)", + content = {@Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = PreAuctionResponse.class)) + )} + ) + }) @ApiResponseExplanations( errors = { - @ApiExceptionExplanation( - value = AuctionErrorCode.class, - constant = AUCTION_ALREADY_REGISTERED, - name = "이미 등록된 경매일때", - description = "이미 등록된 경매 일때 에러가 발생" - ), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY, name = "minutes 파라미터는 진행중인 경매일 때만 사용가능"), } ) - @Operation(summary = "정식 경매 전환") - ResponseEntity startAuction(Long userId, @Valid StartAuctionRequest request); - - @Operation(summary = "테스트 경매 등록") - ResponseEntity testEndAuction(Long userId, int seconds); + @GetMapping + ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(required = false) Category category, + @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, + @RequestParam(required = false) @Min(value = 1, message = "minutes는 1 이상의 값이어야 합니다.") Integer minutes, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "경매 카테고리 조회", description = "경매 카테고리 목록을 조회합니다.") + @GetMapping("/categories") + ResponseEntity> getCategoryList(); + + @Operation(summary = "사용자가 등록한 진행중인 경매 목록 조회", description = "사용자가 등록한 진행중인 경매 목록을 조회합니다.") + @GetMapping("/users/proceeding") + ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 등록한 종료된 경매 목록 조회", description = "사용자가 등록한 종료된 경매 목록을 조회합니다.") + @GetMapping("/users/ended") + ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 등록한 사전 경매 목록 조회", description = "사용자가 등록한 사전 경매 목록을 조회합니다.") + @GetMapping("/users/pre") + ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 낙찰한 경매 목록 조회", description = "사용자가 낙찰한 경매 목록을 조회합니다.") + @GetMapping("/users/won") + ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 낙찰실패한 경매 목록 조회", description = "사용자가 낙찰실패한 경매 목록을 조회합니다.") + @GetMapping("/users/lost") + ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "사용자가 좋아요(찜)한 경매 목록 조회", description = "사용자가 좋아요(찜)한 경매 목록을 조회합니다.") + @GetMapping("/users/likes") + ResponseEntity> getLikedAuctionList(@LoginUser Long userId, + @ParameterObject @PageableDefault(sort = "newest") Pageable pageable); + + @Operation(summary = "경매 등록", description = "경매를 등록합니다.") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = UserErrorCode.class, constant = USER_NOT_FOUND, name = "회원정보 조회 실패"), + } + ) + @PostMapping + ResponseEntity registerAuction(@LoginUser Long userId, + @RequestPart("request") @Valid RegisterRequest request, + @RequestPart(value = "images") @Valid + @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images); + + @Operation(summary = "경매 테스트 등록", description = "테스트 등록합니다.") + @PostMapping("/test") + ResponseEntity testEndAuction(@LoginUser Long userId, + @RequestParam int seconds); } diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java index ab20c751..ea2eb926 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java @@ -1,176 +1,129 @@ package org.chzz.market.domain.auction.controller; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.request.StartAuctionRequest; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.AuctionResponse; +import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; +import org.chzz.market.domain.auction.dto.AuctionRegisterType; +import org.chzz.market.domain.auction.dto.request.RegisterRequest; +import org.chzz.market.domain.auction.dto.response.CategoryResponse; +import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auction.dto.response.ProceedingAuctionResponse; import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auction.service.AuctionRegistrationServiceFactory; -import org.chzz.market.domain.auction.service.AuctionService; -import org.chzz.market.domain.auction.service.register.AuctionRegistrationService; -import org.chzz.market.domain.auction.type.TestService; -import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.bid.service.BidService; -import org.chzz.market.domain.product.entity.Product.Category; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.service.AuctionCategoryService; +import org.chzz.market.domain.auction.service.AuctionLookupService; +import org.chzz.market.domain.auction.service.AuctionMyService; +import org.chzz.market.domain.auction.service.AuctionTestService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -@Slf4j -@RequiredArgsConstructor @RestController -@RequestMapping("/v1/auctions") +@RequiredArgsConstructor +@Validated public class AuctionController implements AuctionApi { - private final AuctionService auctionService; - private final BidService bidService; - private final TestService testService; - private final AuctionRegistrationServiceFactory registrationServiceFactory; + private final AuctionLookupService auctionLookupService; + private final AuctionCategoryService auctionCategoryService; + private final AuctionTestService testService; + private final AuctionMyService auctionMyService; /** * 경매 목록 조회 */ @Override @GetMapping - public ResponseEntity> getAuctionList(@RequestParam Category category, - @LoginUser Long userId, - Pageable pageable) { - return ResponseEntity.ok(auctionService.getAuctionListByCategory(category, userId, pageable)); + public ResponseEntity> getAuctionList(@LoginUser Long userId, + @RequestParam(required = false) Category category, + @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, + @Parameter(description = "경매 종료까지 남은 시간 (분) (1분 이상이어야 함)") + @RequestParam(required = false) @Min(value = 1, message = "minutes는 1 이상의 값이어야 합니다.") Integer minutes, + @PageableDefault(sort = "newest") Pageable pageable) { + return ResponseEntity.ok( + auctionLookupService.getAuctionList(userId, category, status, minutes, pageable)); } /** - * Best 경매 상품 목록 조회 + * 경매 카테고리 Enum 조회 */ @Override - @GetMapping("/best") - public ResponseEntity> bestAuctionList() { - List bestAuctionList = auctionService.getBestAuctionList(); - return ResponseEntity.ok(bestAuctionList); + @GetMapping("/categories") + public ResponseEntity> getCategoryList() { + return ResponseEntity.ok(auctionCategoryService.getCategories()); } /** - * Imminent 경매 상품 목록 조회 + * 사용자가 등록한 진행중인 경매 목록 조회 */ @Override - @GetMapping("/imminent") - public ResponseEntity> imminentAuctionList() { - List imminentAuctionList = auctionService.getImminentAuctionList(); - return ResponseEntity.ok(imminentAuctionList); - } - - /** - * 내가 성공한 경매 조회 - */ - @Override - @GetMapping("/won") - public ResponseEntity> getWonAuctionHistory( - @LoginUser Long userId, - @PageableDefault(size = 20, sort = "newest") Pageable pageable) { - return ResponseEntity.ok(auctionService.getWonAuctionHistory(userId, pageable)); - } - - /** - * 내가 실패한 경매 조회 - */ - @Override - @GetMapping("/lost") - public ResponseEntity> getLostAuctionHistory( - @LoginUser Long userId, - @PageableDefault(size = 20, sort = "newest") Pageable pageable) { - return ResponseEntity.ok(auctionService.getLostAuctionHistory(userId, pageable)); + @GetMapping("/users/proceeding") + public ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserProceedingAuctionList(userId, pageable)); } /** - * 경매 상세 조회 + * 사용자가 등록한 종료된 경매 목록 조회 */ @Override - @GetMapping("/{auctionId}") - public ResponseEntity getAuctionDetails( - @PathVariable Long auctionId, - @LoginUser Long userId) { - return ResponseEntity.ok(auctionService.getFullAuctionDetails(auctionId, userId)); + public ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserEndedAuctionList(userId, pageable)); } /** - * 경매 입찰 목록 조회 + * 사용자가 등록한 사전 경매 목록 조회 */ @Override - @GetMapping("/{auctionId}/bids") - public ResponseEntity> getBids(@LoginUser Long userId, @PathVariable Long auctionId, - Pageable pageable) { - return ResponseEntity.ok(bidService.getBidsByAuctionId(userId, auctionId, pageable)); + @GetMapping("/users/pre") + public ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserPreAuctionList(userId, pageable)); } /** - * 낙찰 정보 조회 + * 사용자가 낙찰한 경매 목록 조회 */ @Override - @GetMapping("/{auctionId}/winning-bid") - public ResponseEntity getWinningBid(@LoginUser Long userId, - @PathVariable Long auctionId) { - return ResponseEntity.ok(auctionService.getWinningBidByAuctionId(userId, auctionId)); + public ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserWonAuctionList(userId, pageable)); } /** - * 사용자가 등록한 모든 경매 목록 조회 현재 사용 X + * 사용자가 낙찰실패한 경매 목록 조회 */ @Override - @GetMapping("/users") - public ResponseEntity> getUserRegisteredAuction(@LoginUser Long userId, - Pageable pageable) { - return ResponseEntity.ok(auctionService.getAuctionListByUserId(userId, pageable)); + public ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, + @PageableDefault(sort = "newest") Pageable pageable) { + return ResponseEntity.ok(auctionMyService.getUserLostAuctionList(userId, pageable)); } /** - * 사용자 경매 상품 목록 조회 (닉네임) 현재 사용 X + * 사용자가 좋아요(찜)한 경매 목록 조회 */ @Override - @GetMapping("/users/{nickname}") - public ResponseEntity> getUserAuctionList(@PathVariable String nickname, + @GetMapping("/users/likes") + public ResponseEntity> getLikedAuctionList(@LoginUser Long userId, @PageableDefault(sort = "newest") Pageable pageable) { - return ResponseEntity.ok(auctionService.getAuctionListByNickname(nickname, pageable)); - } - - /** - * 사용자의 진행 중인 경매 목록 조회 - */ - @Override - @GetMapping("/users/proceeding") - public ResponseEntity> getProceedingAuctions(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return ResponseEntity.ok(auctionService.getProceedingAuctionListByUserId(userId, pageable)); - } - - /** - * 사용자의 종료된 경매 목록 조회 - */ - @Override - @GetMapping("/users/ended") - public ResponseEntity> getEndedAuctions(@LoginUser Long userId, - @PageableDefault(sort = "newest") Pageable pageable) { - return ResponseEntity.ok(auctionService.getEndedAuctionListByUserId(userId, pageable)); + return ResponseEntity.ok(auctionMyService.getLikedAuctionList(userId, pageable)); } /** @@ -178,34 +131,17 @@ public ResponseEntity> getEndedAuctions(@LoginUse */ @Override @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) - public ResponseEntity registerAuction( - @LoginUser Long userId, - @RequestPart("request") @Valid BaseRegisterRequest request, - @RequestPart(value = "images") List images) { - - AuctionRegistrationService auctionRegistrationService = registrationServiceFactory.getService( - request.getAuctionRegisterType()); - RegisterResponse response = auctionRegistrationService.register(userId, request, images); - - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - /** - * 경매 상품으로 전환 - */ - @Override - @PostMapping("/start") - public ResponseEntity startAuction(@LoginUser Long userId, - @RequestBody @Valid StartAuctionRequest request) { - StartAuctionResponse response = auctionService.startAuction(userId, request); - log.info("경매 상품으로 성공적으로 전환되었습니다. 상품 ID: {}", response.productId()); - return ResponseEntity.status(HttpStatus.CREATED).body(response); + public ResponseEntity registerAuction(@LoginUser Long userId, + @RequestPart("request") @Valid RegisterRequest request, + @RequestPart(value = "images") @Valid + @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images) { + AuctionRegisterType type = request.auctionRegisterType(); + type.getService().register(userId, request, images);//요청 타입에 따라 다른 서비스 호출 + return ResponseEntity.status(HttpStatus.CREATED).build(); } -// --------------------------------------------------------------------------------------- - /** - * 경매 종료 테스트 API (삭제 필요) + * 경매 테스트 등록 */ @Override @PostMapping("/test") diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java similarity index 78% rename from src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java rename to src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java index 50eec8fb..f4d45443 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java @@ -1,18 +1,19 @@ -package org.chzz.market.domain.auctionv2.controller; +package org.chzz.market.domain.auction.controller; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ACCESS_FORBIDDEN; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ALREADY_OFFICIAL; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_ENDED; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.INVALID_IMAGE_COUNT; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.MAX_IMAGE_COUNT_EXCEEDED; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NOT_A_PRE_AUCTION; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NOW_WINNER; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.NO_IMAGES_PROVIDED; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.OFFICIAL_AUCTION_DELETE_FORBIDDEN; -import static org.chzz.market.domain.imagev2.error.ImageErrorCode.Const.IMAGE_DELETE_FAILED; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_NOT_ENDED; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.INVALID_IMAGE_COUNT; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.MAX_IMAGE_COUNT_EXCEEDED; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.NOT_A_PRE_AUCTION; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.NOT_WINNER; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.NO_IMAGES_PROVIDED; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.OFFICIAL_AUCTION_DELETE_FORBIDDEN; +import static org.chzz.market.domain.image.error.ImageErrorCode.Const.IMAGE_DELETE_FAILED; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -23,14 +24,14 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; -import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; -import org.chzz.market.domain.auctionv2.dto.response.UpdateAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; +import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auction.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionDetailResponse; +import org.chzz.market.domain.auction.dto.response.UpdateAuctionResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.error.AuctionErrorCode; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.imagev2.error.ImageErrorCode; +import org.chzz.market.domain.image.error.ImageErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -43,7 +44,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; -@Tag(name = "auctions(v2)", description = "V2 경매 API") +@Tag(name = "auctions", description = "경매 API") public interface AuctionDetailApi { @Operation(summary = "특정 경매 상세 조회", description = "특정 경매 상세 정보를 조회합니다.") @ApiResponses(value = { @@ -68,7 +69,7 @@ ResponseEntity> getBids(@LoginUser Long userId, @Operation(summary = "특정 경매 낙찰 조회", description = "특정 경매 낙찰 정보를 조회합니다.") @ApiResponseExplanations( errors = { - @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = NOW_WINNER, name = "낙찰자가 아닐때"), + @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = NOT_WINNER, name = "낙찰자가 아닐때"), @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), } ) @@ -96,6 +97,11 @@ ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Long auctionId); @Operation(summary = "특정 경매 수정", description = "특정 경매를 수정합니다.") + @Parameter( + name = "sequence (예: 1)", + description = "key: 이미지 순서(1~5), value: 업로드할 이미지 파일", + schema = @Schema(type = "string", format = "binary") + ) @ApiResponseExplanations( errors = { @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = AUCTION_NOT_FOUND, name = "경매를 찾을 수 없는 경우"), diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java similarity index 83% rename from src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java rename to src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java index 3da14a19..3818066c 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java @@ -1,21 +1,21 @@ -package org.chzz.market.domain.auctionv2.controller; +package org.chzz.market.domain.auction.controller; import static org.springframework.data.domain.Sort.Direction.DESC; import java.util.Map; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; -import org.chzz.market.domain.auctionv2.dto.response.UpdateAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auctionv2.service.AuctionDeleteService; -import org.chzz.market.domain.auctionv2.service.AuctionDetailService; -import org.chzz.market.domain.auctionv2.service.AuctionModifyService; -import org.chzz.market.domain.auctionv2.service.AuctionStartService; -import org.chzz.market.domain.auctionv2.service.AuctionWonService; +import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auction.dto.response.UpdateAuctionResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.service.AuctionDeleteService; +import org.chzz.market.domain.auction.service.AuctionDetailService; +import org.chzz.market.domain.auction.service.AuctionModifyService; +import org.chzz.market.domain.auction.service.AuctionStartService; +import org.chzz.market.domain.auction.service.AuctionWonService; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.bid.service.BidLookupService; -import org.chzz.market.domain.likev2.service.LikeUpdateService; +import org.chzz.market.domain.like.service.LikeUpdateService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -32,7 +32,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/v2/auctions/{auctionId}") +@RequestMapping("/v1/auctions/{auctionId}") public class AuctionDetailController implements AuctionDetailApi { private final AuctionDetailService auctionDetailService; private final AuctionDeleteService auctionDeleteService; @@ -81,7 +81,7 @@ public ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Lo @Override @PatchMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity updateAuction(Long userId,@PathVariable Long auctionId, + public ResponseEntity updateAuction(Long userId, @PathVariable Long auctionId, UpdateAuctionRequest request, Map images) { UpdateAuctionResponse response = diff --git a/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java b/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java new file mode 100644 index 00000000..ccce5396 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java @@ -0,0 +1,11 @@ +package org.chzz.market.domain.auction.dto; + +import java.util.Map; +import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auction.entity.Auction; +import org.springframework.web.multipart.MultipartFile; + +public record AuctionImageUpdateEvent(Auction auction, + UpdateAuctionRequest request, + Map imageBuffer) { +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java b/src/main/java/org/chzz/market/domain/auction/dto/AuctionRegisterType.java similarity index 63% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java rename to src/main/java/org/chzz/market/domain/auction/dto/AuctionRegisterType.java index c0f32226..dc7be1c6 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegisterType.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/AuctionRegisterType.java @@ -1,11 +1,11 @@ -package org.chzz.market.domain.auctionv2.dto; +package org.chzz.market.domain.auction.dto; import lombok.AllArgsConstructor; import lombok.Getter; import org.chzz.market.common.util.ApplicationContextProvider; -import org.chzz.market.domain.auctionv2.service.AuctionRegistrationService; -import org.chzz.market.domain.auctionv2.service.PreAuctionRegistrationService; -import org.chzz.market.domain.auctionv2.service.RegistrationService; +import org.chzz.market.domain.auction.service.AuctionRegistrationService; +import org.chzz.market.domain.auction.service.PreAuctionRegistrationService; +import org.chzz.market.domain.auction.service.RegistrationService; @Getter @AllArgsConstructor diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java b/src/main/java/org/chzz/market/domain/auction/dto/AuctionRegistrationEvent.java similarity index 75% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java rename to src/main/java/org/chzz/market/domain/auction/dto/AuctionRegistrationEvent.java index 09f20600..0cb61c40 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionRegistrationEvent.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/AuctionRegistrationEvent.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto; +package org.chzz.market.domain.auction.dto; import java.time.LocalDateTime; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/BaseAuctionDto.java b/src/main/java/org/chzz/market/domain/auction/dto/BaseAuctionDto.java deleted file mode 100644 index b6ad89bb..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/BaseAuctionDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.chzz.market.domain.auction.dto; - -import lombok.Getter; -import lombok.ToString; - -@Getter -@ToString -public abstract class BaseAuctionDto { - protected String productName; - protected String imageUrl; - protected Long timeRemaining; - protected Long minPrice; - protected Long participantCount; - - public BaseAuctionDto(String productName, String imageUrl, Long timeRemaining, Long minPrice, Long participantCount) { - this.productName = productName; - this.imageUrl = imageUrl; - this.timeRemaining = timeRemaining; - this.minPrice = minPrice; - this.participantCount = participantCount; - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java b/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java new file mode 100644 index 00000000..ed237add --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java @@ -0,0 +1,8 @@ +package org.chzz.market.domain.auction.dto; + +import java.util.List; +import org.chzz.market.domain.auction.entity.Auction; +import org.springframework.web.multipart.MultipartFile; + +public record ImageUploadEvent(Auction auction, List images) { +} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequest.java deleted file mode 100644 index df09b089..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.chzz.market.domain.auction.dto.request; - -import static org.chzz.market.domain.product.entity.Product.Category; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import org.chzz.market.common.validation.annotation.ThousandMultiple; -import org.chzz.market.domain.auction.type.AuctionRegisterType; - -@Getter -@SuperBuilder -@NoArgsConstructor -@AllArgsConstructor -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "auctionRegisterType", visible = true) -@JsonSubTypes({ - @JsonSubTypes.Type(name = "PRE_REGISTER", value = PreRegisterRequest.class), - @JsonSubTypes.Type(name = "REGISTER", value = RegisterAuctionRequest.class) -}) -public abstract class BaseRegisterRequest { - public static final String DESCRIPTION_REGEX = "^(?:(?:[^\\n]*\\n){0,10}[^\\n]*$)"; // 개행문자 10개를 제한 - - @NotBlank - @Size(min = 2, max = 30, message = "제목은 최소 2글자 이상 30자 이하여야 합니다") - protected String productName; - - @Schema(description = "개행문자 포함 최대 1000자, 개행문자 최대 10개") - @Size(max = 1000, message = "상품설명은 1000자 이내여야 합니다.") - @Pattern(regexp = DESCRIPTION_REGEX, message = "줄 바꿈 10번까지 가능합니다") - protected String description; - - @NotNull(message = "카테고리를 선택해주세요") - protected Category category; - - @NotNull - @ThousandMultiple - @Max(value = 2_000_000, message = "최소금액은 200만원을 넘을 수 없습니다") - protected Integer minPrice; - - @NotNull(message = "경매 타입을 선택해주세요") - protected AuctionRegisterType auctionRegisterType; -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/PreRegisterRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/PreRegisterRequest.java deleted file mode 100644 index 796c9e2a..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/PreRegisterRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.chzz.market.domain.auction.dto.request; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Getter -@SuperBuilder -@NoArgsConstructor -public class PreRegisterRequest extends BaseRegisterRequest { -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterAuctionRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterAuctionRequest.java deleted file mode 100644 index d6d43cf7..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterAuctionRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.chzz.market.domain.auction.dto.request; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Getter -@SuperBuilder -@NoArgsConstructor -public class RegisterAuctionRequest extends BaseRegisterRequest { -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java similarity index 86% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java rename to src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java index b49a9ab7..ff02ac35 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/request/RegisterRequest.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.request; +package org.chzz.market.domain.auction.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; @@ -6,8 +6,8 @@ import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import org.chzz.market.common.validation.annotation.ThousandMultiple; -import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.dto.AuctionRegisterType; +import org.chzz.market.domain.auction.entity.Category; public record RegisterRequest( String productName, diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/StartAuctionRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/StartAuctionRequest.java deleted file mode 100644 index 8994b8ca..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/StartAuctionRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.chzz.market.domain.auction.dto.request; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class StartAuctionRequest { - @NotNull - private Long productId; -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java similarity index 84% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java rename to src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java index 7c4a4a48..4d25611f 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/request/UpdateAuctionRequest.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java @@ -1,6 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.request; - -import static org.chzz.market.domain.auction.dto.request.BaseRegisterRequest.DESCRIPTION_REGEX; +package org.chzz.market.domain.auction.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; @@ -13,13 +11,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.chzz.market.common.validation.annotation.ThousandMultiple; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.entity.Category; @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class UpdateAuctionRequest { + public static final String DESCRIPTION_REGEX = "^(?:(?:[^\n]\n){0,10}[^\n]$)"; // 개행문자 10개를 제한 + @Size(min = 2, max = 30, message = "제목은 최소 2글자 이상 30자 이하여야 합니다") private String productName; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionDetailsResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionDetailsResponse.java deleted file mode 100644 index 4eeae887..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionDetailsResponse.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.querydsl.core.annotations.QueryProjection; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.ArrayList; -import java.util.List; -import lombok.Getter; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.product.entity.Product.Category; - -@Getter -public class AuctionDetailsResponse { - private final Long productId; - private final String sellerNickname; - private final String sellerProfileImageUrl; - private final String productName; - private final String description; - private final Integer minPrice; - private final Category category; - private final Long timeRemaining; - private final AuctionStatus status; - private final Boolean isSeller; - private final Long participantCount; - private final Boolean isParticipated; - private final Long bidId; - private final Long bidAmount; - private final int remainingBidCount; - private final Boolean isCancelled; - - @Schema(description = "낙찰자인지 여부") - private final Boolean isWinner; - - @Schema(description = "낙찰되었는지 여부") - private final Boolean isWon; - - @Schema(description = "주문 여부 - 판매자와 낙찰자에게만 제공") - @JsonInclude(JsonInclude.Include.NON_NULL) - private Boolean isOrdered; - - private List images = new ArrayList<>(); - - @QueryProjection - public AuctionDetailsResponse(Long productId, String sellerNickname, String sellerProfileImageUrl, - String productName, String description, - Integer minPrice, Category category, Long timeRemaining, AuctionStatus status, - Boolean isSeller, - Long participantCount, Boolean isParticipated, Long bidId, Long bidAmount, - int remainingBidCount, Boolean isCancelled, Boolean isWinner, Boolean isWon, - Boolean isOrdered) { - this.productId = productId; - this.sellerNickname = sellerNickname; - this.sellerProfileImageUrl = sellerProfileImageUrl; - this.productName = productName; - this.description = description; - this.minPrice = minPrice; - this.category = category; - this.timeRemaining = timeRemaining; - this.status = status; - this.isSeller = isSeller; - this.participantCount = participantCount; - this.isParticipated = isParticipated; - this.bidId = bidId; - this.bidAmount = bidAmount; - this.remainingBidCount = remainingBidCount; - this.isCancelled = isCancelled; - this.isWinner = isWinner; - this.isWon = isWon; - this.isOrdered = isOrdered; - } - - public AuctionDetailsResponse clearOrderIfNotEligible() { - if (!isSeller && !isWinner) { - this.isOrdered = null; - } - return this; - } - - public void addImageList(List images) { - this.images = images; - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionParticipationResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionParticipationResponse.java deleted file mode 100644 index 62adaad3..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionParticipationResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import com.querydsl.core.annotations.QueryProjection; -import org.chzz.market.domain.auction.type.AuctionStatus; - -public record AuctionParticipationResponse ( - AuctionStatus status, - Long winnerId, - Long count -) { - @QueryProjection - public AuctionParticipationResponse {} -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionResponse.java deleted file mode 100644 index 212c3ef2..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/AuctionResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.querydsl.core.annotations.QueryProjection; -import lombok.Getter; -import org.chzz.market.domain.auction.dto.BaseAuctionDto; - -/** - * 진행중인 경매 목록 조회 DTO - */ -@Getter -public class AuctionResponse extends BaseAuctionDto { - private final Long auctionId; - @JsonInclude(Include.NON_NULL) - private Boolean isParticipated; - - @QueryProjection - public AuctionResponse(Long auctionId, String name, String cdnPath, Long timeRemaining, Long minPrice, - Long participantCount, Boolean isParticipated) { - super(name, cdnPath, timeRemaining, minPrice, participantCount); - this.auctionId = auctionId; - this.isParticipated = isParticipated; - } - - @QueryProjection - public AuctionResponse(Long auctionId, String name, String cdnPath, Long timeRemaining, Long minPrice, - Long participantCount) { - super(name, cdnPath, timeRemaining, minPrice, participantCount); - this.auctionId = auctionId; - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java similarity index 84% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java index 71feea38..6fe1f9ba 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionDetailResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java @@ -1,11 +1,11 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.image.dto.ImageResponse; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.image.dto.response.ImageResponse; @Getter @NoArgsConstructor diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java similarity index 91% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java index b883a28d..5619a4c3 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/BaseAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/CategoryResponse.java similarity index 84% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/CategoryResponse.java index fd543eb7..0aa2e025 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/CategoryResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/CategoryResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java similarity index 94% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java index d903427f..cf667a2a 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/EndedAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import java.time.LocalDateTime; import lombok.Getter; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java index a0c91a52..abca1eaa 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java @@ -1,18 +1,21 @@ package org.chzz.market.domain.auction.dto.response; -import com.querydsl.core.annotations.QueryProjection; - import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class LostAuctionResponse extends BaseAuctionResponse { + private Long participantCount; + private LocalDateTime endDateTime; + private Long bidAmount; -public record LostAuctionResponse ( - Long auctionId, - String productName, - String imageUrl, - Integer minPrice, - Long participantCount, - LocalDateTime endDateTime, - Long bidAmount -) { - @QueryProjection - public LostAuctionResponse {} + public LostAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long participantCount, LocalDateTime endDateTime, Long bidAmount) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.participantCount = participantCount; + this.endDateTime = endDateTime; + this.bidAmount = bidAmount; + } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java similarity index 92% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java index 282a0bdf..fc4b7aac 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionDetailResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java @@ -1,11 +1,11 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; @Getter @NoArgsConstructor diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java similarity index 92% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java index 2285110a..8c515aa6 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/OfficialAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java similarity index 84% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java index 48aeff6d..7dfc9299 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionDetailResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java @@ -1,10 +1,10 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; @Getter @NoArgsConstructor diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java similarity index 90% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java index eaeed98a..2ff2c626 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/PreAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/PreRegisterResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/PreRegisterResponse.java deleted file mode 100644 index 7910ead6..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/PreRegisterResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import lombok.Getter; - -/** - * 사전 등록 DTO - */ -public record PreRegisterResponse(Long productId, String message) implements RegisterResponse { - private static final String PRE_REGISTER_SUCCESS_MESSAGE = "상품이 성공적으로 사전 등록되었습니다."; - - public static PreRegisterResponse of(Long productId) { - return new PreRegisterResponse(productId, PRE_REGISTER_SUCCESS_MESSAGE); - } - - @Override - public Long getProductId() { - return productId; - } - - @Override - public String getMessage() { - return message; - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java similarity index 88% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java index 53eb9cb2..09f7320f 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ProceedingAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java @@ -1,9 +1,9 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.AuctionStatus; @Getter @NoArgsConstructor diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/RegisterAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/RegisterAuctionResponse.java deleted file mode 100644 index 113d5092..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/RegisterAuctionResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import org.chzz.market.domain.auction.type.AuctionStatus; - -/** - * 경매 등록 DTO - */ -public record RegisterAuctionResponse(Long productId, Long auctionId, AuctionStatus status, String message) implements RegisterResponse { - private static final String AUCTION_SUCCESS_MESSAGE = "상품이 성공적으로 경매 등록되었습니다."; - - public static RegisterAuctionResponse of(Long productId, Long auctionId, AuctionStatus status) { - return new RegisterAuctionResponse(productId, auctionId, status, AUCTION_SUCCESS_MESSAGE); - } - - @Override - public Long getProductId() { - return productId; - } - - @Override - public String getMessage() { - return message; - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/RegisterResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/RegisterResponse.java deleted file mode 100644 index 5af2e941..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/RegisterResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -public sealed interface RegisterResponse permits RegisterAuctionResponse, PreRegisterResponse { - Long getProductId(); - String getMessage(); -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/SimpleAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/SimpleAuctionResponse.java deleted file mode 100644 index e08bd99e..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/SimpleAuctionResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import com.querydsl.core.annotations.QueryProjection; - -public record SimpleAuctionResponse ( - String imageUrl, - String productName, - Integer minPrice, - Long participantCount -) { - @QueryProjection - public SimpleAuctionResponse {} -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/StartAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/StartAuctionResponse.java deleted file mode 100644 index ad820414..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/StartAuctionResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import org.chzz.market.domain.auction.type.AuctionStatus; - -import java.time.LocalDateTime; - -/** - * 경매 시작 (사전 등록 -> 경매 등록 전환) DTO - */ -public record StartAuctionResponse( - Long auctionId, // 사전 경매 전환 시 응답 값에 auctionId 사용 - Long productId, - AuctionStatus status, - LocalDateTime endDateTime, - String message -) { - private static final String DEFAULT_SUCCESS_MESSAGE = "경매가 성공적으로 시작되었습니다."; - - public static StartAuctionResponse of(Long auctionId, Long productId, AuctionStatus status, LocalDateTime endDateTime) { - return new StartAuctionResponse(auctionId, productId, status, endDateTime, DEFAULT_SUCCESS_MESSAGE); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/UpdateAuctionResponse.java similarity index 64% rename from src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java rename to src/main/java/org/chzz/market/domain/auction/dto/response/UpdateAuctionResponse.java index 7706de6c..a1fd2e9a 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/UpdateAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/UpdateAuctionResponse.java @@ -1,8 +1,9 @@ -package org.chzz.market.domain.auctionv2.dto.response; +package org.chzz.market.domain.auction.dto.response; import java.util.List; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.image.dto.response.ImageResponse; public record UpdateAuctionResponse( Long auctionId, @@ -10,9 +11,9 @@ public record UpdateAuctionResponse( String description, Category category, Integer minPrice, - List imageUrls + List imageUrls ) { - public static UpdateAuctionResponse from(AuctionV2 auction) { + public static UpdateAuctionResponse from(Auction auction) { return new UpdateAuctionResponse( auction.getId(), auction.getName(), diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/UserAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/UserAuctionResponse.java deleted file mode 100644 index b56c4f33..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/UserAuctionResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import com.querydsl.core.annotations.QueryProjection; -import java.time.LocalDateTime; -import lombok.Getter; -import lombok.ToString; -import org.chzz.market.domain.auction.dto.BaseAuctionDto; - -import org.chzz.market.domain.auction.type.AuctionStatus; - -/** - * 나의 경매 목록 조회 DTO - */ -@Getter -@ToString -public class UserAuctionResponse extends BaseAuctionDto { - private final Long auctionId; - private final AuctionStatus status; - private final LocalDateTime createdAt; - - @QueryProjection - public UserAuctionResponse(Long auctionId, String name, String cdnPath, Long timeRemaining, Long minPrice, - Long participantCount, AuctionStatus status, LocalDateTime createdAt) { - super(name, cdnPath, timeRemaining, minPrice, participantCount); - this.auctionId = auctionId; - this.status = status; - this.createdAt = createdAt; - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/UserEndedAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/UserEndedAuctionResponse.java deleted file mode 100644 index d94b59d1..00000000 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/UserEndedAuctionResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.chzz.market.domain.auction.dto.response; - -import com.querydsl.core.annotations.QueryProjection; -import java.time.LocalDateTime; - -/** - * 사용자의 종료된 경매 목록 조회 - */ -public record UserEndedAuctionResponse( - Long auctionId, - String productName, - String imageUrl, - Long minPrice, - Long participantCount, - Long winningBidAmount, - Boolean isWon, // 낙찰 유무 - Boolean isOrdered, // 주문 유무 - LocalDateTime createAt -) { - - @QueryProjection - public UserEndedAuctionResponse { - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java index f2f4c01c..e281fbfd 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java @@ -9,5 +9,6 @@ public record WonAuctionDetailsResponse( Long winningAmount ) { @QueryProjection - public WonAuctionDetailsResponse {} + public WonAuctionDetailsResponse { + } } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java index 49bab8cb..dad12a41 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java @@ -1,20 +1,26 @@ package org.chzz.market.domain.auction.dto.response; -import com.querydsl.core.annotations.QueryProjection; - import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class WonAuctionResponse extends BaseAuctionResponse { + private Long participantCount; + private LocalDateTime endDateTime; + private Long winningAmount; + private Boolean isOrdered; + private Long orderId; -public record WonAuctionResponse ( - Long auctionId, - String productName, - String imageUrl, - Integer minPrice, - Long participantCount, - LocalDateTime endDateTime, - Long winningAmount, - Boolean isOrdered, - Long orderId -) { - @QueryProjection - public WonAuctionResponse {} + public WonAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long participantCount, LocalDateTime endDateTime, Long winningAmount, Boolean isOrdered, + Long orderId) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.participantCount = participantCount; + this.endDateTime = endDateTime; + this.winningAmount = winningAmount; + this.isOrdered = isOrdered; + this.orderId = orderId; + } } diff --git a/src/main/java/org/chzz/market/domain/auction/entity/Auction.java b/src/main/java/org/chzz/market/domain/auction/entity/Auction.java index 3b48b10e..f2d9088a 100644 --- a/src/main/java/org/chzz/market/domain/auction/entity/Auction.java +++ b/src/main/java/org/chzz/market/domain/auction/entity/Auction.java @@ -1,54 +1,75 @@ package org.chzz.market.domain.auction.entity; +import static org.chzz.market.domain.auction.entity.AuctionStatus.ENDED; +import static org.chzz.market.domain.auction.entity.AuctionStatus.PRE; +import static org.chzz.market.domain.auction.entity.AuctionStatus.PROCEEDING; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ENDED; import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_ENDED; -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.chzz.market.domain.auction.entity.listener.AuctionEntityListener; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auction.error.AuctionErrorCode; import org.chzz.market.domain.auction.error.AuctionException; -import org.chzz.market.domain.auction.type.AuctionStatus; import org.chzz.market.domain.base.entity.BaseTimeEntity; -import org.chzz.market.domain.product.entity.Product; +import org.chzz.market.domain.image.entity.Image; +import org.chzz.market.domain.image.error.ImageErrorCode; +import org.chzz.market.domain.image.error.exception.ImageException; +import org.chzz.market.domain.user.entity.User; +import org.hibernate.annotations.DynamicUpdate; -@Getter +@Table @Entity -@Table(indexes = { - @Index(name = "idx_auction_end_date_time", columnList = "end_date_time") -}) -@Builder @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(value = AuctionEntityListener.class) +@Builder +@DynamicUpdate +@Getter +@Slf4j public class Auction extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "auction_id") private Long id; - @OneToOne - @JoinColumn(name = "product_id") - private Product product; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "seller_id", nullable = false) + private User seller; + + @Column(nullable = false) + private String name; + + @Column(length = 1000) + private String description; @Column - private Long winnerId; + private Integer minPrice; + + @Column(nullable = false, columnDefinition = "varchar(30)") + @Enumerated(EnumType.STRING) + private Category category; @Column private LocalDateTime endDateTime; @@ -57,55 +78,113 @@ public class Auction extends BaseTimeEntity { @Enumerated(EnumType.STRING) private AuctionStatus status; - public Integer getMinPrice() { - return product.getMinPrice(); + @Column + private Long winnerId; + + @Builder.Default + @Column + private Long likeCount = 0L; + + @Builder.Default + @Column + private Long bidCount = 0L; + + @Builder.Default + @OneToMany(mappedBy = "auction", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval = true) + private List images = new ArrayList<>(); + + public void addImage(Image image) { + images.add(image); + image.specifyAuction(this); + } + + public boolean isOwner(Long userId) { + return seller.getId().equals(userId); + } + + public void validateOwner(Long userId) { + if (!isOwner(userId)) { + throw new AuctionException(AUCTION_ACCESS_FORBIDDEN); + } + } + + public boolean isPreAuction() { + return status == PRE; + } + + public boolean isOfficialAuction() { + return status == PROCEEDING || status == ENDED; + } + + public void validateAuctionEnded() { + if (!status.equals(ENDED)) { + throw new AuctionException(AUCTION_NOT_ENDED); + } } - public static Auction toEntity(Product product) { - return Auction.builder() - .product(product) - .status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusHours(24)) - .build(); + public boolean isWinner(Long userId) { + return winnerId != null && winnerId.equals(userId); + } + + public void startOfficialAuction() { + if (isOfficialAuction()) { + throw new AuctionException(AUCTION_ALREADY_OFFICIAL); + } + this.status = PROCEEDING; + this.endDateTime = LocalDateTime.now().plusDays(1); + } + + public String getFirstImageCdnPath() { + return images.stream() + .filter(image -> image.getSequence() == 1) + .map(Image::getCdnPath) + .findFirst() + .orElseThrow(() -> { + log.error("경매의 첫 번째 이미지가 없는 경우: {}", this.id); + return new ImageException(ImageErrorCode.IMAGE_NOT_FOUND); + }); } public void validateAuctionEndTime() { // 경매가 진행중이 아닐 때 - if (status != PROCEEDING || LocalDateTime.now().isAfter(endDateTime)) { + if (status != PROCEEDING || endDateTime == null || LocalDateTime.now().isAfter(endDateTime)) { throw new AuctionException(AUCTION_ENDED); } } - // 입찰 금액이 최소 금액 이상인지 확인 public boolean isAboveMinPrice(Long amount) { - return amount >= getMinPrice(); + return amount >= minPrice; + } + + public void addImages(final List images) { + this.images.addAll(images); } public void endAuction() { - this.status = AuctionStatus.ENDED; + this.status = ENDED; } - public void assignWinner(Long winnerId) { - this.winnerId = winnerId; + public void assignWinner(final Long bidderId) { + this.winnerId = bidderId; } - public void validateAuctionEnded() { - if (!this.status.equals(AuctionStatus.ENDED)) { - throw new AuctionException(AUCTION_NOT_ENDED); - } + public void update(final UpdateAuctionRequest request) { + this.name = request.getProductName(); + this.description = request.getDescription(); + this.category = request.getCategory(); + this.minPrice = request.getMinPrice(); } - /** - * 경매가 진행중인지 확인 - */ - public boolean isProceeding() { - return status == PROCEEDING && LocalDateTime.now().isBefore(endDateTime); + public void validateImageSize() { + int count = this.images.size(); + if (count < 1) { + throw new AuctionException(AuctionErrorCode.NO_IMAGES_PROVIDED); + } else if (count > 5) { + throw new AuctionException(AuctionErrorCode.MAX_IMAGE_COUNT_EXCEEDED); + } } - /** - * 낙찰자인지 확인 - */ - public boolean isWinner(Long userId) { - return winnerId != null && winnerId.equals(userId); + public void removeImages(final List imagesToRemove) { + this.images.removeAll(imagesToRemove); } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionStatus.java b/src/main/java/org/chzz/market/domain/auction/entity/AuctionStatus.java similarity index 82% rename from src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionStatus.java rename to src/main/java/org/chzz/market/domain/auction/entity/AuctionStatus.java index 49c4711b..21257f39 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionStatus.java +++ b/src/main/java/org/chzz/market/domain/auction/entity/AuctionStatus.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.entity; +package org.chzz.market.domain.auction.entity; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/Category.java b/src/main/java/org/chzz/market/domain/auction/entity/Category.java similarity index 90% rename from src/main/java/org/chzz/market/domain/auctionv2/entity/Category.java rename to src/main/java/org/chzz/market/domain/auction/entity/Category.java index e51c6920..a60c5a47 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/Category.java +++ b/src/main/java/org/chzz/market/domain/auction/entity/Category.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.entity; +package org.chzz.market.domain.auction.entity; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/org/chzz/market/domain/auction/entity/listener/AuctionEntityListener.java b/src/main/java/org/chzz/market/domain/auction/entity/listener/AuctionEntityListener.java deleted file mode 100644 index d431ef9c..00000000 --- a/src/main/java/org/chzz/market/domain/auction/entity/listener/AuctionEntityListener.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.chzz.market.domain.auction.entity.listener; - -import jakarta.persistence.PostPersist; -import java.sql.Date; -import java.time.ZoneId; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.schedule.AuctionEndJob; -import org.quartz.JobBuilder; -import org.quartz.JobDetail; -import org.quartz.Scheduler; -import org.quartz.SchedulerException; -import org.quartz.SimpleTrigger; -import org.quartz.TriggerBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -public class AuctionEntityListener { - @Autowired - private Scheduler scheduler; - - @PostPersist - public void postPersist(Auction auction) { - // Job과 Trigger를 스케줄러에 등록 - try { - // JobDetail 생성 - JobDetail jobDetail = JobBuilder.newJob(AuctionEndJob.class) - .withIdentity("auctionEndJob_" + auction.getId(), "auctionJobs") - .usingJobData("auctionId", String.valueOf(auction.getId())) // auctionId를 문자열로 변환하여 저장 - .build(); - - // Trigger 생성 - SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() - .withIdentity("auctionEndTrigger_" + auction.getId(), "auctionTriggers") - .startAt(Date.from(auction.getEndDateTime().atZone(ZoneId.systemDefault()).toInstant())) - .build(); - scheduler.scheduleJob(jobDetail, trigger); - log.info("Scheduled job with ID: {} and Trigger: {} at {}", jobDetail.getKey(), trigger.getKey(), - auction.getEndDateTime()); - } catch (SchedulerException e) { - log.error("SchedulerException occurred while scheduling job", e); - } - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auction/error/AuctionErrorCode.java index 41329207..437c0c28 100644 --- a/src/main/java/org/chzz/market/domain/auction/error/AuctionErrorCode.java +++ b/src/main/java/org/chzz/market/domain/auction/error/AuctionErrorCode.java @@ -12,26 +12,35 @@ @Getter @AllArgsConstructor public enum AuctionErrorCode implements ErrorCode { - AUCTION_ENDED(BAD_REQUEST, "경매가 종료되었습니다."), - AUCTION_NOT_FOUND(NOT_FOUND, "경매를 찾을 수 없습니다."), - INVALID_AUCTION_STATE(BAD_REQUEST, "경매 상태가 유효하지 않습니다."), - AUCTION_ALREADY_REGISTERED(BAD_REQUEST, "이미 등록된 경매입니다."), - UNKNOWN_AUCTION_TYPE(BAD_REQUEST, "알 수 없는 경매 타입입니다."), - AUCTION_NOT_ENDED(BAD_REQUEST, "아직 경매가 종료되지 않았습니다."), + AUCTION_NOT_ENDED(BAD_REQUEST, "해당 경매가 아직 끝나지 않았습니다."), + AUCTION_ALREADY_OFFICIAL(BAD_REQUEST, "해당 경매는 이미 정식 경매입니다."), + AUCTION_ENDED(BAD_REQUEST, "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), + END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY(BAD_REQUEST, + "진행중인 경매 목록 조회 시에만 minutes 파라미터를 사용할 수 있습니다."), + INVALID_IMAGE_COUNT(HttpStatus.BAD_REQUEST, "이미지 개수가 올바르지 않습니다."), + MAX_IMAGE_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지는 최대 5개까지 등록할 수 있습니다."), + NOT_A_PRE_AUCTION(BAD_REQUEST, "사전 등록 경매가 아닙니다"), + NO_IMAGES_PROVIDED(HttpStatus.BAD_REQUEST, "이미지가 제공되지 않았습니다."), + OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), NOT_WINNER(FORBIDDEN, "낙찰자가 아닙니다."), - FORBIDDEN_AUCTION_ACCESS(FORBIDDEN, "해당 경매에 접근할 수 없습니다."); + AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), + AUCTION_NOT_FOUND(NOT_FOUND, "경매를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String message; public static class Const { - public static final String AUCTION_ENDED = "AUCTION_ENDED"; - public static final String AUCTION_NOT_FOUND = "AUCTION_NOT_FOUND"; - public static final String INVALID_AUCTION_STATE = "INVALID_AUCTION_STATE"; - public static final String AUCTION_ALREADY_REGISTERED = "AUCTION_ALREADY_REGISTERED"; - public static final String UNKNOWN_AUCTION_TYPE = "UNKNOWN_AUCTION_TYPE"; public static final String AUCTION_NOT_ENDED = "AUCTION_NOT_ENDED"; + public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; + public static final String AUCTION_ENDED = "AUCTION_ENDED"; + public static final String END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY = "END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY"; + public static final String INVALID_IMAGE_COUNT = "INVALID_IMAGE_COUNT"; + public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; + public static final String MAX_IMAGE_COUNT_EXCEEDED = "MAX_IMAGE_COUNT_EXCEEDED"; + public static final String NOT_A_PRE_AUCTION = "NOT_A_PRE_AUCTION"; + public static final String NO_IMAGES_PROVIDED = "NO_IMAGES_PROVIDED"; public static final String NOT_WINNER = "NOT_WINNER"; - public static final String FORBIDDEN_AUCTION_ACCESS = "FORBIDDEN_AUCTION_ACCESS"; + public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; + public static final String AUCTION_NOT_FOUND = "AUCTION_NOT_FOUND"; } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java similarity index 50% rename from src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java rename to src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java index 2622dd7f..1f33bdd7 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java @@ -1,30 +1,32 @@ -package org.chzz.market.domain.auctionv2.repository; +package org.chzz.market.domain.auction.repository; -import static com.querydsl.core.types.dsl.Expressions.numberTemplate; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.ENDED; -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PRE; -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; +import static org.chzz.market.domain.auction.entity.AuctionStatus.ENDED; +import static org.chzz.market.domain.auction.entity.AuctionStatus.PRE; +import static org.chzz.market.domain.auction.entity.AuctionStatus.PROCEEDING; +import static org.chzz.market.domain.auction.entity.QAuction.auction; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; import static org.chzz.market.domain.bid.entity.QBid.bid; -import static org.chzz.market.domain.image.entity.QImageV2.imageV2; -import static org.chzz.market.domain.likev2.entity.QLikeV2.likeV2; -import static org.chzz.market.domain.orderv2.entity.QOrderV2.orderV2; +import static org.chzz.market.domain.image.entity.QImage.image; +import static org.chzz.market.domain.like.entity.QLike.like; +import static org.chzz.market.domain.order.entity.QOrder.order; import static org.chzz.market.domain.user.entity.QUser.user; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Ops.DateTimeOps; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.DateTimeOperation; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import lombok.AccessLevel; @@ -33,21 +35,22 @@ import lombok.RequiredArgsConstructor; import org.chzz.market.common.util.QuerydslOrder; import org.chzz.market.common.util.QuerydslOrderProvider; -import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; -import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionDetailResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.QWonAuctionDetailsResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; +import org.chzz.market.domain.auction.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auction.dto.response.OfficialAuctionResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionDetailResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auction.dto.response.ProceedingAuctionResponse; +import org.chzz.market.domain.auction.dto.response.QWonAuctionDetailsResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.bid.entity.QBid; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.image.dto.QImageResponse; +import org.chzz.market.domain.image.dto.response.ImageResponse; +import org.chzz.market.domain.image.dto.response.QImageResponse; +import org.chzz.market.domain.user.dto.response.ParticipationCountsResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; @@ -55,7 +58,7 @@ @Repository @RequiredArgsConstructor -public class AuctionV2QueryRepository { +public class AuctionQueryRepository { private final JPAQueryFactory jpaQueryFactory; private final QuerydslOrderProvider querydslOrderProvider; @@ -64,12 +67,12 @@ public class AuctionV2QueryRepository { */ public Optional findWinningBidById(Long auctionId) { return Optional.ofNullable(jpaQueryFactory.select( - new QWonAuctionDetailsResponse(auctionV2.id, auctionV2.name, imageV2.cdnPath, bid.amount)) - .from(auctionV2) - .leftJoin(bid).on(bid.bidderId.eq(auctionV2.winnerId) - .and(bid.auctionId.eq(auctionV2.id))) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) - .where(auctionV2.id.eq(auctionId)) + new QWonAuctionDetailsResponse(auction.id, auction.name, image.cdnPath, bid.amount)) + .from(auction) + .leftJoin(bid).on(bid.bidderId.eq(auction.winnerId) + .and(bid.auctionId.eq(auction.id))) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) + .where(auction.id.eq(auctionId)) .fetchOne()); } @@ -81,24 +84,24 @@ public Optional findPreAuctionDetailById(Long userId, .select( Projections.constructor( PreAuctionDetailResponse.class, - auctionV2.id, + auction.id, user.nickname, user.profileImageUrl, - auctionV2.name, - auctionV2.description, - auctionV2.minPrice, + auction.name, + auction.description, + auction.minPrice, userIdEq(userId), - auctionV2.status, - auctionV2.category, - auctionV2.updatedAt, - auctionV2.likeCount, - likeV2.id.isNotNull() + auction.status, + auction.category, + auction.updatedAt, + auction.likeCount, + like.id.isNotNull() ) ) - .from(auctionV2) - .join(auctionV2.seller, user) - .leftJoin(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeUserIdEq(userId))) - .where(auctionV2.id.eq(auctionId)) + .from(auction) + .join(auction.seller, user) + .leftJoin(like).on(like.auctionId.eq(auction.id).and(likeUserIdEq(userId))) + .where(auction.id.eq(auctionId)) .fetchOne()); result.ifPresent(response -> response.addImageList(getImagesByAuctionId(response.getAuctionId()))); @@ -115,37 +118,37 @@ public Optional findOfficialAuctionDetailById(Lon .select( Projections.constructor( OfficialAuctionDetailResponse.class, - auctionV2.id, + auction.id, user.nickname, user.profileImageUrl, - auctionV2.name, - auctionV2.description, - auctionV2.minPrice, + auction.name, + auction.description, + auction.minPrice, userIdEq(userId), - auctionV2.status, - auctionV2.category, + auction.status, + auction.category, timeRemaining().longValue(), - auctionV2.bidCount, + auction.bidCount, activeBid.id.isNotNull(), activeBid.id, activeBid.amount.coalesce(0L), activeBid.count.coalesce(3), canceledBid.id.isNotNull(), winnerIdEq(userId), - auctionV2.winnerId.isNotNull(), - orderV2.isNotNull() + auction.winnerId.isNotNull(), + order.isNotNull() ) ) - .from(auctionV2) - .join(auctionV2.seller, user) + .from(auction) + .join(auction.seller, user) .leftJoin(activeBid).on(activeBid.auctionId.eq(auctionId) // 활성화된 입찰 조인 .and(activeBid.status.eq(ACTIVE)) .and(bidderIdEqSub(activeBid, userId))) .leftJoin(canceledBid).on(canceledBid.auctionId.eq(auctionId) // 취소된 입찰 조인 .and(canceledBid.status.eq(CANCELLED)) .and(bidderIdEqSub(canceledBid, userId))) - .leftJoin(orderV2).on(orderV2.auction.eq(auctionV2)) - .where(auctionV2.id.eq(auctionId)) + .leftJoin(order).on(order.auction.eq(auction)) + .where(auction.id.eq(auctionId)) .fetchOne()); officialAuctionDetailResponse.ifPresent( @@ -158,31 +161,31 @@ public Optional findOfficialAuctionDetailById(Lon * 사전 경매 목록 조회 */ public Page findPreAuctions(Long userId, Category category, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(PRE))); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .where(categoryEqIgnoreNull(category).and(auction.status.eq(PRE))); List content = baseQuery .select( Projections.constructor( PreAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), - auctionV2.likeCount, - likeV2.id.isNotNull() + auction.likeCount, + like.id.isNotNull() ) ) - .join(auctionV2.seller, user) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) - .leftJoin(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeUserIdEq(userId))) + .join(auction.seller, user) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) + .leftJoin(like).on(like.auctionId.eq(auction.id).and(likeUserIdEq(userId))) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -193,33 +196,33 @@ public Page findPreAuctions(Long userId, Category category, public Page findOfficialAuctions(Long userId, Category category, AuctionStatus status, Integer endWithinSeconds, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .where(categoryEqIgnoreNull(category).and(auctionV2.status.eq(status)) + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .where(categoryEqIgnoreNull(category).and(auction.status.eq(status)) .and(timeRemainingIgnoreNull(endWithinSeconds))); List content = baseQuery .select( Projections.constructor( OfficialAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), timeRemaining().longValue(), - auctionV2.bidCount, + auction.bidCount, bid.id.isNotNull() ) ) - .join(auctionV2.seller, user) - .leftJoin(bid).on(bid.auctionId.eq(auctionV2.id).and(bidderIdEq(userId)).and(bid.status.eq(ACTIVE))) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .join(auction.seller, user) + .leftJoin(bid).on(bid.auctionId.eq(auction.id).and(bidderIdEq(userId)).and(bid.status.eq(ACTIVE))) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -228,30 +231,30 @@ public Page findOfficialAuctions(Long userId, Category * 사용자가 등록한 사전경매 목록 조회 */ public Page findPreAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .join(auctionV2.seller, user).on(user.id.eq(userId)) - .where(auctionV2.status.eq(PRE)); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .join(auction.seller, user).on(user.id.eq(userId)) + .where(auction.status.eq(PRE)); List content = baseQuery .select( Projections.constructor( PreAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), Expressions.TRUE, - auctionV2.likeCount, + auction.likeCount, Expressions.FALSE ) ) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -260,31 +263,31 @@ public Page findPreAuctionsByUserId(Long userId, Pageable pa * 사용자가 좋아요한 사전 경매목록 조회 */ public Page findLikedAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .join(likeV2).on(likeV2.auctionId.eq(auctionV2.id).and(likeV2.userId.eq(userId))) - .where(auctionV2.status.eq(PRE)); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .join(like).on(like.auctionId.eq(auction.id).and(like.userId.eq(userId))) + .where(auction.status.eq(PRE)); List content = baseQuery .select( Projections.constructor( PreAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), - auctionV2.likeCount, - likeV2.id.isNotNull() + auction.likeCount, + like.id.isNotNull() ) ) - .join(auctionV2.seller, user) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .join(auction.seller, user) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -293,32 +296,32 @@ public Page findLikedAuctionsByUserId(Long userId, Pageable * 사용자가 등록한 진행 중인 경매 목록 조회 */ public Page findProceedingAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .join(auctionV2.seller, user).on(user.id.eq(userId)) - .where(auctionV2.status.eq(PROCEEDING)); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .join(auction.seller, user).on(user.id.eq(userId)) + .where(auction.status.eq(PROCEEDING)); List content = baseQuery .select( Projections.constructor( ProceedingAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), timeRemaining().longValue(), - auctionV2.status, - auctionV2.bidCount, - auctionV2.createdAt + auction.status, + auction.bidCount, + auction.createdAt ) ) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -327,34 +330,34 @@ public Page findProceedingAuctionsByUserId(Long userI * 사용자가 등록한 종료된 경매 목록 조회 */ public Page findEndedAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .join(auctionV2.seller, user).on(user.id.eq(userId)) - .where(auctionV2.status.eq(ENDED)); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .join(auction.seller, user).on(user.id.eq(userId)) + .where(auction.status.eq(ENDED)); List content = baseQuery .select( Projections.constructor( EndedAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), - auctionV2.bidCount, + auction.bidCount, getWinningBidAmount(), - auctionV2.winnerId.isNotNull(), - orderV2.isNotNull(), - auctionV2.createdAt + auction.winnerId.isNotNull(), + order.isNotNull(), + auction.createdAt ) ) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) - .leftJoin(orderV2).on(orderV2.auction.eq(auctionV2)) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) + .leftJoin(order).on(order.auction.eq(auction)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -363,35 +366,35 @@ public Page findEndedAuctionsByUserId(Long userId, Pageabl * 사용자가 낙찰한 경매 목록 조회 */ public Page findWonAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .join(bid).on(bid.auctionId.eq(auctionV2.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) - .where(auctionV2.winnerId.eq(userId).and(auctionV2.status.eq(ENDED))); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .join(bid).on(bid.auctionId.eq(auction.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) + .where(auction.winnerId.eq(userId).and(auction.status.eq(ENDED))); List content = baseQuery .select( Projections.constructor( WonAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), - auctionV2.bidCount, - auctionV2.endDateTime, + auction.bidCount, + auction.endDateTime, bid.amount, - orderV2.isNotNull(), - orderV2.id + order.isNotNull(), + order.id ) ) - .join(auctionV2.seller, user) - .leftJoin(orderV2).on(orderV2.auction.id.eq(auctionV2.id)) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .join(auction.seller, user) + .leftJoin(order).on(order.auction.id.eq(auction.id)) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } @@ -401,42 +404,97 @@ public Page findWonAuctionsByUserId(Long userId, Pageable pa * 사용자가 낙찰 실패한 경매 목록 조회 */ public Page findLostAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auctionV2) - .join(bid).on(bid.auctionId.eq(auctionV2.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) - .where(auctionV2.winnerId.ne(userId).and(auctionV2.status.eq(ENDED))); + JPAQuery baseQuery = jpaQueryFactory.from(auction) + .join(bid).on(bid.auctionId.eq(auction.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) + .where(auction.winnerId.ne(userId).and(auction.status.eq(ENDED))); List content = baseQuery .select( Projections.constructor( LostAuctionResponse.class, - auctionV2.id, - auctionV2.name, - imageV2.cdnPath, - auctionV2.minPrice.longValue(), + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), userIdEq(userId), - auctionV2.bidCount, - auctionV2.endDateTime, + auction.bidCount, + auction.endDateTime, bid.amount ) ) - .join(auctionV2.seller, user) - .leftJoin(auctionV2.images, imageV2).on(imageV2.sequence.eq(1)) + .join(auction.seller, user) + .leftJoin(auction.images, image).on(image.sequence.eq(1)) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery.select(auctionV2.count()); + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } + /** + * 사용자가 참여한 경매 통계 조회 + */ + public ParticipationCountsResponse getParticipationCounts(Long userId) { + DateTimeOperation now = Expressions.dateTimeOperation(LocalDateTime.class, + DateTimeOps.CURRENT_TIMESTAMP); + + BooleanExpression isEnded = auction.status.eq(ENDED) + .and(auction.endDateTime.before(now)); + + // 사용자가 참여한 경매 ID 목록을 가져옵니다. + List participatedAuctionIds = jpaQueryFactory + .select(auction.id) + .from(auction) + .join(bid).on(bid.auctionId.eq(auction.id)) + .where(bid.bidderId.eq(userId) + .and(bid.status.eq(ACTIVE))) + .fetch(); + + BooleanExpression isParticipatedAuction = auction.id.in(participatedAuctionIds); + + Long proceedingCount = Optional.ofNullable(jpaQueryFactory + .select(auction.count()) + .from(auction) + .where(isParticipatedAuction + .and(auction.status.eq(PROCEEDING)) + .and(auction.endDateTime.after(now))) + .fetchFirst()) + .orElse(0L); + + Long successCount = Optional.ofNullable(jpaQueryFactory + .select(auction.count()) + .from(auction) + .where(isParticipatedAuction + .and(auction.winnerId.eq(userId)) + .and(isEnded)) + .fetchFirst()) + .orElse(0L); + + Long failureCount = Optional.ofNullable(jpaQueryFactory + .select(auction.count()) + .from(auction) + .where(isParticipatedAuction + .and(auction.winnerId.ne(userId)) + .and(isEnded)) + .fetchFirst()) + .orElse(0L); + + return new ParticipationCountsResponse( + proceedingCount, + successCount, + failureCount + ); + } + private List getImagesByAuctionId(Long auctionId) { return jpaQueryFactory - .select(new QImageResponse(imageV2.id, imageV2.cdnPath)) - .from(imageV2) - .where(imageV2.auction.id.eq(auctionId)) - .orderBy(imageV2.sequence.asc()) + .select(new QImageResponse(image.id, image.cdnPath)) + .from(image) + .where(image.auction.id.eq(auctionId)) + .orderBy(image.sequence.asc()) .fetch(); } @@ -453,15 +511,15 @@ private BooleanBuilder bidderIdEqSub(QBid qBid, Long userId) { } private BooleanBuilder winnerIdEq(Long userId) { - return nullSafeBuilder(() -> auctionV2.winnerId.isNotNull().and(auctionV2.winnerId.eq(userId))); + return nullSafeBuilder(() -> auction.winnerId.isNotNull().and(auction.winnerId.eq(userId))); } private BooleanBuilder likeUserIdEq(Long userId) { - return nullSafeBuilder(() -> likeV2.userId.eq(userId)); + return nullSafeBuilder(() -> like.userId.eq(userId)); } private BooleanBuilder categoryEqIgnoreNull(Category category) { - return nullSafeBuilderIgnore(() -> auctionV2.category.eq(category)); + return nullSafeBuilderIgnore(() -> auction.category.eq(category)); } private BooleanExpression timeRemainingIgnoreNull(Integer endWithinSeconds) { @@ -469,15 +527,15 @@ private BooleanExpression timeRemainingIgnoreNull(Integer endWithinSeconds) { } private static NumberExpression timeRemaining() { - return numberTemplate(Integer.class, - "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 + return Expressions.numberTemplate(Integer.class, + "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auction.endDateTime); // 음수면 0으로 처리 } private JPQLQuery getWinningBidAmount() { return JPAExpressions.select(bid.amount.max().coalesce(0L)) .from(bid) .where( - bid.auctionId.eq(auctionV2.id), + bid.auctionId.eq(auction.id), bid.status.eq(ACTIVE) ); } @@ -485,11 +543,11 @@ private JPQLQuery getWinningBidAmount() { @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum AuctionOrder implements QuerydslOrder { - POPULARITY("popularity-v2", auctionV2.bidCount.desc()), - EXPENSIVE("expensive-v2", auctionV2.minPrice.desc()), - CHEAP("cheap-v2", auctionV2.minPrice.asc()), - IMMEDIATELY("immediately-v2", timeRemaining().asc()), - NEWEST("newest-v2", auctionV2.createdAt.desc()); + POPULARITY("popularity", auction.bidCount.desc()), + EXPENSIVE("expensive", auction.minPrice.desc()), + CHEAP("cheap", auction.minPrice.asc()), + IMMEDIATELY("immediately", timeRemaining().asc()), + NEWEST("newest", auction.createdAt.desc()); private final String name; private final OrderSpecifier orderSpecifier; diff --git a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepository.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepository.java index 47944122..eec0c02d 100644 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepository.java +++ b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepository.java @@ -1,16 +1,31 @@ package org.chzz.market.domain.auction.repository; +import java.util.Optional; import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; -public interface AuctionRepository extends JpaRepository, AuctionRepositoryCustom { - /* - * 상품 아이디로 경매가 존재하는지 확인 - */ - boolean existsByProductId(Long productId); +public interface AuctionRepository extends JpaRepository { + @Query("SELECT a.status FROM Auction a WHERE a.id = :auctionId") + Optional findAuctionStatusById(Long auctionId); - /* - * 사용자가 등록한 경매 수 조회 - */ - long countByProductUserId(Long userId); + @Modifying + @Query("UPDATE Auction a SET a.likeCount = a.likeCount + 1 WHERE a.id = :auctionId") + void incrementLikeCount(Long auctionId); + + @Modifying + @Query("UPDATE Auction a SET a.likeCount = a.likeCount - 1 WHERE a.id = :auctionId AND a.likeCount > 0") + void decrementLikeCount(Long auctionId); + + @Modifying + @Query("UPDATE Auction a SET a.bidCount = a.bidCount + 1 WHERE a.id = :auctionId") + void incrementBidCount(Long auctionId); + + @Modifying + @Query("UPDATE Auction a SET a.bidCount = a.bidCount - 1 WHERE a.id = :auctionId AND a.bidCount > 0") + void decrementBidCount(Long auctionId); + + long countBySellerIdAndStatusIn(Long userId, AuctionStatus... status); } diff --git a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustom.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustom.java deleted file mode 100644 index d62a08d0..00000000 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustom.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.chzz.market.domain.auction.repository; - -import java.util.List; -import java.util.Optional; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.AuctionResponse; -import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.SimpleAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.user.dto.response.ParticipationCountsResponse; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface AuctionRepositoryCustom { - /** - * 카테고리에 따라 경매 리스트를 조회합니다. - * - * @param category 카테고리 - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 경매 응답 리스트 - */ - Page findAuctionsByCategory(Category category, Long userId, Pageable pageable); - - /** - * 경매 ID와 사용자 ID로 경매 상세 정보를 조회합니다. - * - * @param auctionId 경매 ID - * @param userId 사용자 ID - * @return 경매 상세 응답 - */ - Optional findAuctionDetailsById(Long auctionId, Long userId); - - /** - * 경매 ID로 경매 간단 상세 정보를 조회합니다. - * - * @param auctionId 경매 ID - * @return 경매 간단 상세정보 응답 - */ - Optional findSimpleAuctionDetailsById(Long auctionId); - - /** - * 사용자 닉네임에 따라 경매 리스트를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @param pageable 페이징 정보 - * @return 페이징된 사용자 경매 응답 리스트 - */ - Page findAuctionsByNickname(String nickname, Pageable pageable); - - /** - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 사용자 경매 등록 기록 - */ - Page findAuctionsByUserId(Long userId, Pageable pageable); - - /** - * 홈 화면의 베스트 경매 조회 - * - * @return 입찰 기록이 많은 경매 정보 - */ - List findBestAuctions(); - - /** - * 홈 화면의 임박 경매 조회 - * - * @return 경매 종료까지 1시간 이내인 경매 정보 - */ - List findImminentAuctions(); - - /** - * 사용자가 낙찰한 경매 이력을 조회합니다. - * - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 낙찰 경매 응답 리스트 - */ - Page findWonAuctionHistoryByUserId(Long userId, Pageable pageable); - - /** - * 사용자가 낙찰하지 못한 경매 이력을 조회합니다. - * - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 낙찰 실패 경매 응답 리스트 - */ - Page findLostAuctionHistoryByUserId(Long userId, Pageable pageable); - - /** - * @param userId - 사용자 ID - * @return 사용자가 참여한 상태별 경매들의 수 - */ - ParticipationCountsResponse getParticipationCounts(Long userId); - - /** - * 주어진 사용자 ID에 해당하는 진행 중인 경매 목록을 페이징하여 조회합니다. - * - * @param userId 경매를 조회할 사용자 ID - * @param pageable 페이징 정보 (페이지 번호, 페이지 크기 등) - * @return 진행 중인 경매 목록을 포함한 페이징 결과 - */ - Page findProceedingAuctionByUserId(Long userId, Pageable pageable); - - /** - * 사용자 ID에 해당하는 종료된 경매 목록을 페이징하여 조회합니다. - * - * @param userId 경매를 조회할 사용자 ID - * @param pageable 페이징 정보 (페이지 번호, 페이지 크기 등) - * @return 종료된 경매 목록을 포함한 페이징 결과 - */ - Page findEndedAuctionByUserId(Long userId, Pageable pageable); - - /** - * 낙찰 정보 조회합니다. - */ - Optional findWinningBidById(Long auctionId); -} diff --git a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java deleted file mode 100644 index 81ba4e81..00000000 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java +++ /dev/null @@ -1,609 +0,0 @@ -package org.chzz.market.domain.auction.repository; - -import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; -import static org.chzz.market.domain.auction.entity.QAuction.auction; -import static org.chzz.market.domain.auction.repository.AuctionRepositoryCustomImpl.AuctionOrder.NEWEST; -import static org.chzz.market.domain.auction.repository.AuctionRepositoryCustomImpl.AuctionOrder.POPULARITY; -import static org.chzz.market.domain.auction.type.AuctionStatus.ENDED; -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; -import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; -import static org.chzz.market.domain.bid.entity.QBid.bid; -import static org.chzz.market.domain.image.entity.QImage.image; -import static org.chzz.market.domain.order.entity.QOrder.order; -import static org.chzz.market.domain.product.entity.QProduct.product; -import static org.chzz.market.domain.user.entity.QUser.user; - -import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.types.Ops.DateTimeOps; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.DateTimeOperation; -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.core.types.dsl.NumberExpression; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.JPQLQuery; -import com.querydsl.jpa.impl.JPAQuery; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.chzz.market.common.util.QuerydslOrder; -import org.chzz.market.common.util.QuerydslOrderProvider; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.AuctionResponse; -import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.QAuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.QAuctionResponse; -import org.chzz.market.domain.auction.dto.response.QLostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.QSimpleAuctionResponse; -import org.chzz.market.domain.auction.dto.response.QUserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.QUserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.QWonAuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.QWonAuctionResponse; -import org.chzz.market.domain.auction.dto.response.SimpleAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; -import org.chzz.market.domain.bid.entity.QBid; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.image.dto.QImageResponse; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.user.dto.response.ParticipationCountsResponse; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.support.PageableExecutionUtils; - -@RequiredArgsConstructor -public class AuctionRepositoryCustomImpl implements AuctionRepositoryCustom { - private final JPAQueryFactory jpaQueryFactory; - private final QuerydslOrderProvider querydslOrderProvider; - - /** - * 카테고리와 정렬 조건에 따라 경매 리스트를 조회합니다. - * - * @param category 카테고리 - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 경매 응답 리스트 - */ - @Override - public Page findAuctionsByCategory(Category category, Long userId, - Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auction) - .join(auction.product, product) - .where(auction.product.category.eq(category).and(auction.status.eq(PROCEEDING))); - - List content = baseQuery - .select(new QAuctionResponse( - auction.id, - product.name, - image.cdnPath, - timeRemaining().longValue(), - product.minPrice.longValue(), - bid.countDistinct(), - isParticipating(userId) - )) - .leftJoin(bid).on(bid.auctionId.eq(auction.id).and(bid.status.eq(ACTIVE))) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .groupBy(auction.id, product.name, image.cdnPath, auction.createdAt, product.minPrice) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery - .select(auction.count()); - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); - } - - /** - * 경매 ID와 사용자 ID로 경매 상세 정보를 조회합니다. - * - * @param auctionId 경매 ID - * @param userId 사용자 ID - * @return 경매 상세 응답 - */ - @Override - public Optional findAuctionDetailsById(Long auctionId, Long userId) { - QBid activeBid = new QBid("bidActive"); - QBid canceledBid = new QBid("bidCanceled"); - Optional auctionDetailsResponse = Optional.ofNullable(jpaQueryFactory - .select(new QAuctionDetailsResponse( - product.id, - user.nickname, - user.profileImageUrl, - product.name, - product.description, - product.minPrice, - product.category, - timeRemaining().longValue(), - auction.status, - userIdEq(userId), - getBidCount(), - activeBid.id.isNotNull(), - activeBid.id, - activeBid.amount.coalesce(0L), - activeBid.count.coalesce(3), - canceledBid.id.isNotNull(), - winnerIdEq(userId), // 낙찰자 여부 - auction.winnerId.isNotNull(), //경매의 낙찰 여부 - order.isNotNull() // 주문 여부 - )) - .from(auction) - .join(auction.product, product) - .join(product.user, user) - // 활성화된 입찰 조인 - .leftJoin(activeBid).on(activeBid.auctionId.eq(auctionId) - .and(activeBid.status.eq(ACTIVE)) // ACTIVE 상태인 입찰만 조인 - .and(bidderIdEqSub(activeBid, userId))) - // 취소된 입찰 조인 - .leftJoin(canceledBid).on(canceledBid.auctionId.eq(auctionId) - .and(canceledBid.status.eq(CANCELLED)) // CANCELED 상태인 입찰 조인 - .and(bidderIdEqSub(canceledBid, userId))) - .leftJoin(order).on(order.auction.eq(auction)) - .where(auction.id.eq(auctionId)) - .fetchOne()); - - auctionDetailsResponse.ifPresent( - response -> response.addImageList(getImagesByProductId(response.getProductId()))); - - return auctionDetailsResponse; - } - - /** - * 경매 ID와 사용자 ID로 경매 간단 상세 정보를 조회합니다. - * - * @param auctionId 경매 ID - * @return 경매 간단 상세정보 응답 - */ - @Override - public Optional findSimpleAuctionDetailsById(Long auctionId) { - return Optional.ofNullable(jpaQueryFactory - .select(new QSimpleAuctionResponse( - image.cdnPath, - product.name, - product.minPrice, - bid.countDistinct() - )) - .from(auction) - .join(auction.product, product) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .leftJoin(bid).on(bid.auctionId.eq(auctionId).and(bid.status.eq(ACTIVE))) - .where(auction.id.eq(auctionId)) - .groupBy(product.name, image.cdnPath, product.minPrice) - .fetchOne()); - } - - /** - * 사용자 닉네임에 따라 경매 리스트를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @param pageable 페이징 정보 - * @return 페이징된 사용자 경매 응답 리스트 - */ - @Override - public Page findAuctionsByNickname(String nickname, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auction) - .join(auction.product, product) - .join(product.user, user) - .where(user.nickname.eq(nickname)); - - return getUserAuctionResponses(pageable, baseQuery); - } - - /** - * 사용자 인증정보를 통해 사용자가 등록한 경매 리스트를 조회합니다. - * - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 사용자 경매 응답 리스트 - */ - @Override - public Page findAuctionsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(auction) - .join(auction.product, product) - .join(product.user, user) - .on(user.id.eq(userId)); - - return getUserAuctionResponses(pageable, baseQuery); - } - - private Page getUserAuctionResponses(Pageable pageable, JPAQuery baseQuery) { - JPAQuery contentQuery = baseQuery - .select(new QUserAuctionResponse( - auction.id, - product.name, - image.cdnPath, - timeRemaining().longValue(), - product.minPrice.longValue(), - getBidCount(), - auction.status, - auction.createdAt)); - - List content = contentQuery - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery.select(auction.count()); - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); - } - - /** - * 홈 화면의 베스트 경매 조회 - * - * @return 입찰 기록이 많은 10개의 경매 정보 - */ - @Override - public List findBestAuctions() { - JPAQuery baseQuery = jpaQueryFactory.from(auction) - .join(auction.product, product) - .where(auction.status.eq(PROCEEDING)) - .orderBy(POPULARITY.getOrderSpecifier(), NEWEST.getOrderSpecifier()); - - return baseQuery.select(new QAuctionResponse( - auction.id, - product.name, - image.cdnPath, - timeRemaining().longValue(), - product.minPrice.longValue(), - bid.countDistinct()) - ) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .leftJoin(bid).on(bid.auctionId.eq(auction.id).and(bid.status.ne(CANCELLED))) - .groupBy(auction.id, product.name, image.cdnPath, auction.createdAt, product.minPrice) - .offset(0) - .limit(5) - .fetch(); - } - - /** - * 홈 화면의 임박 경매 조회 - * - * @return 경매 종료까지 1시간 이내인 경매 정보 - */ - @Override - public List findImminentAuctions() { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) - .join(auction.product, product) - .where( - timeRemaining().between(0, 3600) - .and(auction.status.eq(PROCEEDING))) - .orderBy(timeRemaining().asc(), POPULARITY.getOrderSpecifier()); - - return baseQuery.select(new QAuctionResponse( - auction.id, - product.name, - image.cdnPath, - timeRemaining().longValue(), - product.minPrice.longValue(), - bid.countDistinct()) - ) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .leftJoin(bid).on(bid.auctionId.eq(auction.id).and(bid.status.ne(CANCELLED))) - .groupBy(auction.id, product.name, image.cdnPath) - .offset(0) - .limit(5) - .fetch(); - } - - /** - * 사용자가 낙찰한 경매 이력을 조회합니다. - * - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 낙찰 경매 응답 리스트 - */ - @Override - public Page findWonAuctionHistoryByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) - .join(bid).on(bid.auctionId.eq(auction.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) - .join(auction.product, product) - .where(auction.winnerId.eq(userId).and(auction.status.eq(ENDED))); - - List content = baseQuery - .select(new QWonAuctionResponse( - auction.id, - product.name, - image.cdnPath, - product.minPrice, - getBidCount(), - auction.endDateTime, - bid.amount, - order.isNotNull(), - order.id - )) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .leftJoin(order).on(order.auction.id.eq(auction.id)) - .groupBy(auction.id, product.name, image.cdnPath, product.minPrice, bid.amount, order.id) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery - .select(auction.count()); - - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); - } - - /** - * 사용자가 낙찰하지 못한 경매 이력을 조회합니다. - * - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 낙찰 경매 응답 리스트 - */ - @Override - public Page findLostAuctionHistoryByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) - .join(bid).on(bid.auctionId.eq(auction.id).and(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE)))) - .where(auction.winnerId.ne(userId).and(auction.status.eq(ENDED))); - - List query = baseQuery - .select(new QLostAuctionResponse( - auction.id, - product.name, - image.cdnPath, - product.minPrice, - getBidCount(), - auction.endDateTime, - bid.amount - )) - .join(auction.product, product) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .groupBy(auction.id, product.name, image.cdnPath, product.minPrice, auction.endDateTime) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery - .select(auction.countDistinct()); - - return PageableExecutionUtils.getPage(query, pageable, countQuery::fetchCount); - } - - @Override - public ParticipationCountsResponse getParticipationCounts(Long userId) { - DateTimeOperation now = Expressions.dateTimeOperation(LocalDateTime.class, - DateTimeOps.CURRENT_TIMESTAMP); - - BooleanExpression isEnded = auction.status.eq(ENDED) - .and(auction.endDateTime.before(now)); - - // 사용자가 참여한 경매 ID 목록을 가져옵니다. - List participatedAuctionIds = jpaQueryFactory - .select(auction.id) - .from(auction) - .join(bid).on(bid.auctionId.eq(auction.id)) - .where(bid.bidderId.eq(userId) - .and(bid.status.eq(ACTIVE))) - .fetch(); - - BooleanExpression isParticipatedAuction = auction.id.in(participatedAuctionIds); - - Long proceedingCount = Optional.ofNullable(jpaQueryFactory - .select(auction.count()) - .from(auction) - .where(isParticipatedAuction - .and(auction.status.eq(PROCEEDING)) - .and(auction.endDateTime.after(now))) - .fetchFirst()) - .orElse(0L); - - Long successCount = Optional.ofNullable(jpaQueryFactory - .select(auction.count()) - .from(auction) - .where(isParticipatedAuction - .and(auction.winnerId.eq(userId)) - .and(isEnded)) - .fetchFirst()) - .orElse(0L); - - Long failureCount = Optional.ofNullable(jpaQueryFactory - .select(auction.count()) - .from(auction) - .where(isParticipatedAuction - .and(auction.winnerId.ne(userId)) - .and(isEnded)) - .fetchFirst()) - .orElse(0L); - - return new ParticipationCountsResponse( - proceedingCount, - successCount, - failureCount - ); - } - - @Override - public Page findProceedingAuctionByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) - .join(auction.product, product) - .where(product.user.id.eq(userId).and(auction.status.eq(PROCEEDING))); - - // 진행 중인 경매 조회 쿼리 - List result = baseQuery - .select(new QUserAuctionResponse( - auction.id, - product.name, - image.cdnPath, - timeRemaining().longValue(), - product.minPrice.longValue(), - getBidCount(), - auction.status, - auction.createdAt - )) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - // 전체 경매 수를 계산하는 쿼리 - JPAQuery countQuery = baseQuery - .select(auction.count()); - - return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); - } - - @Override - public Page findEndedAuctionByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) - .join(auction.product, product) - .where(product.user.id.eq(userId).and(auction.status.eq(ENDED))); - - List result = baseQuery - .select(new QUserEndedAuctionResponse( - auction.id, - product.name, - image.cdnPath, - product.minPrice.longValue(), - getBidCount(), - getWinningBidAmount(), - auction.winnerId.isNotNull(), - order.isNotNull(), - auction.createdAt - )) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .leftJoin(order).on(order.auction.eq(auction)) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - // 전체 경매 수를 계산하는 쿼리 - JPAQuery countQuery = baseQuery - .select(auction.count()); - - return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); - } - - @Override - public Optional findWinningBidById(Long auctionId) { - return Optional.ofNullable(jpaQueryFactory.select( - new QWonAuctionDetailsResponse( - auction.id, - product.name, - image.cdnPath, - bid.amount - )) - .from(auction) - .join(bid) - .on(bid.auctionId.eq(auction.id).and(auction.id.eq(auctionId)).and(auction.winnerId.eq(bid.bidderId))) - .join(auction.product, product) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .fetchOne()); - } - - /** - * 상품의 대표 이미지를 조회하기 위한 조건을 반환합니다. - * - * @return 대표 이미지(첫 번째 이미지)의 sequence가 1인 조건식 - */ - private BooleanExpression isRepresentativeImage() { - return image.sequence.eq(1); - } - - /** - * 사용자가 참여 중인 경매인지 확인합니다. - * - * @param userId 사용자 ID - * @return 사용자가 참여 중인 경우 true, 그렇지 않으면 false - */ - private BooleanExpression isParticipating(Long userId) { - return JPAExpressions.selectOne() - .from(bid) - .where(bid.auctionId.eq(auction.id).and(bid.status.eq(ACTIVE).and(bidderIdEq(userId)))) - .exists(); - } - - /** - * 경매 참여자 수를 조회합니다. - * - * @return 참여자 수 - */ - private static NumberExpression getBidCount() { - return Expressions.asNumber(JPAExpressions - .select(bid.count()) - .from(bid) - .where(bid.auctionId.eq(auction.id).and(bid.status.eq(ACTIVE)))); - } - - /** - * 상품의 이미지 리스트를 조회합니다. - */ - private List getImagesByProductId(Long productId) { - return jpaQueryFactory - .select(new QImageResponse(image.id, image.cdnPath)) - .from(image) - .where(image.product.id.eq(productId)) - .orderBy(image.sequence.asc()) - .fetch(); - } - - private static NumberExpression timeRemaining() { - return Expressions.numberTemplate(Integer.class, - "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auction.endDateTime); // 음수면 0으로 처리 - } - - /** - * 경매의 낙찰 금액을 조회합니다. - * - * @return 낙찰금액 - */ - private JPQLQuery getWinningBidAmount() { - return JPAExpressions.select(bid.amount.max().coalesce(0L)) - .from(bid) - .where( - bid.auctionId.eq(auction.id), - bid.status.eq(ACTIVE) - ); - } - - private BooleanBuilder userIdEq(Long userId) { - return nullSafeBuilder(() -> user.id.eq(userId)); - } - - private BooleanBuilder bidderIdEq(Long userId) { - return nullSafeBuilder(() -> bid.bidderId.eq(userId)); - } - - private BooleanBuilder bidderIdEqSub(QBid qBid, Long userId) { - return nullSafeBuilder(() -> qBid.bidderId.eq(userId)); - } - - private BooleanBuilder winnerIdEq(Long userId) { - return nullSafeBuilder(() -> auction.winnerId.isNotNull().and(auction.winnerId.eq(userId))); - } - - private BooleanBuilder buyerIdEq(Long userId) { - return nullSafeBuilder(() -> order.buyerId.eq(userId)); - } - - @Getter - @AllArgsConstructor(access = AccessLevel.PRIVATE) - public enum AuctionOrder implements QuerydslOrder { - POPULARITY("popularity", getBidCount().desc()), - EXPENSIVE("expensive", product.minPrice.desc()), - CHEAP("cheap", product.minPrice.asc()), - NEWEST("newest", auction.createdAt.desc()); - - private final String name; - private final OrderSpecifier orderSpecifier; - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java b/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java index 680d6c01..862d2293 100644 --- a/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java +++ b/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java @@ -1,19 +1,22 @@ package org.chzz.market.domain.auction.schedule; -import org.chzz.market.domain.auction.service.AuctionService; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auction.service.AuctionEndService; import org.quartz.Job; import org.quartz.JobExecutionContext; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +/** + * 경매 스케줄링 종료 작업 + */ @Component +@RequiredArgsConstructor public class AuctionEndJob implements Job { - @Autowired - AuctionService auctionService; + private final AuctionEndService auctionEndService; @Override public void execute(JobExecutionContext context) { Long auctionId = context.getJobDetail().getJobDataMap().getLong("auctionId"); - auctionService.completeAuction(auctionId); + auctionEndService.endAuction(auctionId); } } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionCategoryService.java similarity index 80% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionCategoryService.java index 5be2988d..f588c038 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionCategoryService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionCategoryService.java @@ -1,11 +1,11 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import java.util.Arrays; import java.util.List; import lombok.RequiredArgsConstructor; import org.chzz.market.common.util.StringCaseConverter; -import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.dto.response.CategoryResponse; +import org.chzz.market.domain.auction.entity.Category; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionDeleteService.java similarity index 69% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionDeleteService.java index 7f1e2965..89418a96 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionDeleteService.java @@ -1,15 +1,15 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import static org.chzz.market.domain.notification.entity.NotificationType.PRE_AUCTION_CANCELED; import java.util.List; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.imagev2.service.ImageDeleteService; -import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionErrorCode; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.image.service.ImageDeleteService; +import org.chzz.market.domain.like.repository.LikeRepository; import org.chzz.market.domain.notification.event.NotificationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -20,8 +20,8 @@ @Transactional(readOnly = true) public class AuctionDeleteService { private final ImageDeleteService imageDeleteService; - private final AuctionV2Repository auctionRepository; - private final LikeV2Repository likeRepository; + private final AuctionRepository auctionRepository; + private final LikeRepository likeRepository; private final ApplicationEventPublisher eventPublisher; /** @@ -29,7 +29,7 @@ public class AuctionDeleteService { */ @Transactional public void delete(Long userId, Long auctionId) { - AuctionV2 auction = auctionRepository.findById(auctionId) // TODO: 패치 조인으로 쿼리 성능 개선 필요(Seller, Image) n+1 문제 + Auction auction = auctionRepository.findById(auctionId) // TODO: 패치 조인으로 쿼리 성능 개선 필요(Seller, Image) n+1 문제 .orElseThrow(() -> new AuctionException(AuctionErrorCode.AUCTION_NOT_FOUND)); validate(userId, auction); imageDeleteService.deleteImages(auction.getImages()); @@ -40,7 +40,7 @@ public void delete(Long userId, Long auctionId) { /** * 경매 취소 유효성 검사 */ - private static void validate(Long userId, AuctionV2 auction) { + private static void validate(Long userId, Auction auction) { auction.validateOwner(userId); if (auction.isOfficialAuction()) { throw new AuctionException(AuctionErrorCode.OFFICIAL_AUCTION_DELETE_FORBIDDEN); @@ -50,7 +50,7 @@ private static void validate(Long userId, AuctionV2 auction) { /** * 사전 경매 취소 알림 이벤트 발행 */ - private void processDeleteNotification(AuctionV2 auction) { + private void processDeleteNotification(Auction auction) { // 1. 해당 경매에 좋아요 누른 사용자 ID 추출 List likedUserIds = likeRepository.findByAuctionId(auction.getId()).stream().map(like -> like.getUserId()) .toList(); diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionDetailService.java similarity index 65% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionDetailService.java index cc9af7af..f70df55a 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionDetailService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionDetailService.java @@ -1,14 +1,14 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.dto.response.BaseAuctionDetailResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.dto.response.BaseAuctionDetailResponse; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,8 +16,8 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class AuctionDetailService { - private final AuctionV2Repository auctionRepository; - private final AuctionV2QueryRepository auctionQueryRepository; + private final AuctionRepository auctionRepository; + private final AuctionQueryRepository auctionQueryRepository; public BaseAuctionDetailResponse getAuctionDetails(Long userId, Long auctionId) { return auctionRepository.findAuctionStatusById(auctionId) diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java similarity index 85% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java index 78b644bd..73b8a4d5 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionEndService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_FAILURE; import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_NON_WINNER; @@ -9,10 +9,10 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionErrorCode; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.repository.BidQueryRepository; import org.chzz.market.domain.notification.event.NotificationEvent; @@ -24,13 +24,13 @@ @Component @RequiredArgsConstructor public class AuctionEndService { - private final AuctionV2Repository auctionV2Repository; + private final AuctionRepository auctionRepository; private final BidQueryRepository bidRepository; private final ApplicationEventPublisher eventPublisher; @Transactional public void endAuction(final Long auctionId) { - AuctionV2 auction = auctionV2Repository.findById(auctionId) + Auction auction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionException(AuctionErrorCode.AUCTION_NOT_FOUND)); auction.endAuction(); @@ -40,7 +40,7 @@ public void endAuction(final Long auctionId) { /** * 판매자에게 경매 종료 알림 */ - private void notifyAuctionEnded(AuctionV2 auction) { + private void notifyAuctionEnded(Auction auction) { Long sellerId = auction.getSeller().getId(); String productName = auction.getName(); String firstImageCdnPath = auction.getFirstImageCdnPath(); @@ -66,7 +66,7 @@ private void notifyAuctionEnded(AuctionV2 auction) { /** * 낙찰자에게 알림 전송 */ - private void alter2Winner(AuctionV2 auction, Bid winningBid, String productName, String firstImageCdnPath) { + private void alter2Winner(Auction auction, Bid winningBid, String productName, String firstImageCdnPath) { auction.assignWinner(winningBid.getBidderId()); eventPublisher.publishEvent( NotificationEvent.createAuctionNotification(winningBid.getBidderId(), AUCTION_WINNER, diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionLookupService.java similarity index 69% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionLookupService.java index 75c0dab0..537bbfab 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionLookupService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionLookupService.java @@ -1,12 +1,12 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -16,7 +16,7 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class AuctionLookupService { - private final AuctionV2QueryRepository auctionQueryRepository; + private final AuctionQueryRepository auctionQueryRepository; /** * 경매 목록 조회 diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java similarity index 73% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java index 73d55f0a..910cb912 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionModifyService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java @@ -1,16 +1,16 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import java.util.Collections; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.dto.AuctionImageUpdateEvent; -import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; -import org.chzz.market.domain.auctionv2.dto.response.UpdateAuctionResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.dto.AuctionImageUpdateEvent; +import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auction.dto.response.UpdateAuctionResponse; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionErrorCode; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,7 +20,7 @@ @Service @RequiredArgsConstructor public class AuctionModifyService { - private final AuctionV2Repository auctionV2Repository; + private final AuctionRepository auctionRepository; private final ApplicationEventPublisher eventPublisher; @Transactional @@ -28,14 +28,14 @@ public UpdateAuctionResponse updateAuction(Long userId, Long auctionId, UpdateAuctionRequest request, Map newImages) { // 경매 조회 - AuctionV2 auction = auctionV2Repository.findById(auctionId) + Auction auction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionException(AuctionErrorCode.AUCTION_NOT_FOUND)); // 사용자 권한 체크 auction.validateOwner(userId); // 경매 등록 상태 유무 유효성 검사 - if(!auction.isPreAuction()){ + if (!auction.isPreAuction()) { throw new AuctionException(AuctionErrorCode.NOT_A_PRE_AUCTION); } diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionMyService.java similarity index 77% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionMyService.java index 9a5a75b9..51035c05 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionMyService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionMyService.java @@ -1,12 +1,12 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; +import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auction.dto.response.ProceedingAuctionResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -16,7 +16,7 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class AuctionMyService { - private final AuctionV2QueryRepository auctionQueryRepository; + private final AuctionQueryRepository auctionQueryRepository; /** * 사용자가 등록한 사전 경매 목록 조회 diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java similarity index 71% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java index 5ac57b75..3e7b56c1 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionRegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java @@ -1,15 +1,15 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.dto.AuctionRegistrationEvent; -import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; -import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.dto.AuctionRegistrationEvent; +import org.chzz.market.domain.auction.dto.ImageUploadEvent; +import org.chzz.market.domain.auction.dto.request.RegisterRequest; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.error.UserErrorCode; import org.chzz.market.domain.user.error.exception.UserException; @@ -23,7 +23,7 @@ @Service @RequiredArgsConstructor public class AuctionRegistrationService implements RegistrationService { - private final AuctionV2Repository auctionRepository; + private final AuctionRepository auctionRepository; private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; @@ -33,7 +33,7 @@ public void register(final Long userId, RegisterRequest request, final List new UserException(UserErrorCode.USER_NOT_FOUND)); - AuctionV2 auction = createAuction(request, user); + Auction auction = createAuction(request, user); auctionRepository.save(auction); @@ -42,8 +42,8 @@ public void register(final Long userId, RegisterRequest request, final List preRegisterService; - case REGISTER -> auctionRegisterService; - default -> throw new AuctionException(UNKNOWN_AUCTION_TYPE); - }; - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionSchedulingService.java similarity index 87% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionSchedulingService.java index f03f71a1..a48e268f 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionSchedulingService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionSchedulingService.java @@ -1,12 +1,12 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import java.sql.Date; import java.time.LocalDateTime; import java.time.ZoneId; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.dto.AuctionRegistrationEvent; -import org.chzz.market.domain.auctionv2.schedule.AuctionV2EndJob; +import org.chzz.market.domain.auction.dto.AuctionRegistrationEvent; +import org.chzz.market.domain.auction.schedule.AuctionEndJob; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; @@ -29,7 +29,7 @@ public void registerSchedule(AuctionRegistrationEvent event) { // Job과 Trigger를 스케줄러에 등록 try { // JobDetail 생성 - JobDetail jobDetail = JobBuilder.newJob(AuctionV2EndJob.class) + JobDetail jobDetail = JobBuilder.newJob(AuctionEndJob.class) .withIdentity("auctionEndJob_" + auctionId, "auctionJobs") .usingJobData("auctionId", String.valueOf(auctionId)) // auctionId를 문자열로 변환하여 저장 .build(); diff --git a/src/main/java/org/chzz/market/domain/auction/service/AuctionService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionService.java deleted file mode 100644 index ca30bc53..00000000 --- a/src/main/java/org/chzz/market/domain/auction/service/AuctionService.java +++ /dev/null @@ -1,276 +0,0 @@ -package org.chzz.market.domain.auction.service; - -import static org.chzz.market.common.error.GlobalErrorCode.RESOURCE_NOT_FOUND; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ALREADY_REGISTERED; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.NOT_WINNER; -import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_FAILURE; -import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_NON_WINNER; -import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_START; -import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_SUCCESS; -import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_WINNER; -import static org.chzz.market.domain.product.error.ProductErrorCode.FORBIDDEN_PRODUCT_ACCESS; -import static org.chzz.market.domain.product.error.ProductErrorCode.PRODUCT_NOT_FOUND; - -import java.util.List; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.common.error.GlobalException; -import org.chzz.market.domain.auction.dto.request.StartAuctionRequest; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.AuctionResponse; -import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.SimpleAuctionResponse; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.error.AuctionException; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.bid.entity.Bid; -import org.chzz.market.domain.bid.service.BidService; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.notification.event.NotificationEvent; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.product.error.ProductException; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -@Slf4j -public class AuctionService { - - private final BidService bidService; - private final AuctionRepository auctionRepository; - private final ProductRepository productRepository; - private final ApplicationEventPublisher eventPublisher; - - /** - * 카테고리에 따라 경매 리스트를 조회 - */ - public Page getAuctionListByCategory(Category category, Long userId, - Pageable pageable) { - return auctionRepository.findAuctionsByCategory(category, userId, pageable); - } - - /** - * 경매 상세 정보를 조회 - * TODO: 서비스 추상화 적용 시 참고 (#9 관련) - * 현재 enum 통해 응답 형태 다양화 구현 - * 추후 서비스 추상화 적용 시 이 부분 활용해 구현할 수 있습니다. - */ - public AuctionDetailsResponse getFullAuctionDetails(Long auctionId, Long userId) { - return auctionRepository.findAuctionDetailsById(auctionId, userId) - .map(AuctionDetailsResponse::clearOrderIfNotEligible) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - } - - /** - * 판매자 입찰 화면에 제공되는 경매 간단 상세 정보를 조회 - */ - public SimpleAuctionResponse getSimpleAuctionDetails(Long auctionId) { - return auctionRepository.findSimpleAuctionDetailsById(auctionId) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - } - - /** - * 사용자 닉네임에 따라 경매 리스트를 조회 - */ - public Page getAuctionListByNickname(String nickname, Pageable pageable) { - return auctionRepository.findAuctionsByNickname(nickname, pageable); - } - - /** - * 내가 성공한 경매 조회 - */ - public Page getWonAuctionHistory(Long userId, Pageable pageable) { - return auctionRepository.findWonAuctionHistoryByUserId(userId, pageable); - } - - /** - * 내가 실패한 경매 조회 - */ - public Page getLostAuctionHistory(Long userId, Pageable pageable) { - return auctionRepository.findLostAuctionHistoryByUserId(userId, pageable); - } - - /** - * 베스트 경매 입찰 내역 조회 - */ - public List getBestAuctionList() { - return auctionRepository.findBestAuctions(); - } - - /** - * 경매 종료까지 1시간 이내인(마감임박) 경매 조회 - */ - public List getImminentAuctionList() { - return auctionRepository.findImminentAuctions(); - } - - /** - * 사용자가 등록한 모든 경매 목록 조회 - */ - public Page getAuctionListByUserId(Long userId, Pageable pageable) { - return auctionRepository.findAuctionsByUserId(userId, pageable); - } - - /** - * 사용자가 등록한 진행중인 경매 목록 조회 - */ - public Page getProceedingAuctionListByUserId(Long userId, Pageable pageable) { - return auctionRepository.findProceedingAuctionByUserId(userId, pageable); - } - - /** - * 사용자가 등록한 종료된 경매 목록 조회 - */ - public Page getEndedAuctionListByUserId(Long userId, Pageable pageable) { - return auctionRepository.findEndedAuctionByUserId(userId, pageable); - } - - /** - * 낙찰 정보 조회 - */ - public WonAuctionDetailsResponse getWinningBidByAuctionId(Long userId, Long auctionId) { - Auction auction = getAuctionById(auctionId); - if (!auction.isWinner(userId)) { - throw new AuctionException(NOT_WINNER); - } - return auctionRepository.findWinningBidById(auctionId) - .orElseThrow(() -> new GlobalException(RESOURCE_NOT_FOUND)); - } - - /** - * 사전 등록 상품 경매 전환 처리 - */ - @Transactional - public StartAuctionResponse startAuction(Long userId, StartAuctionRequest request) { - Product product = validateStartAuction(request.getProductId(), userId); - return changeAuction(product); - } - - @Transactional - public StartAuctionResponse changeAuction(Product product) { - log.info("사전 등록 상품을 경매 등록 상품으로 전환하기 시작합니다. 상품 ID: {}", product.getId()); - - Auction auction = Auction.toEntity(product); - auction = auctionRepository.save(auction); - - // 좋아요 누른 사용자 ID 추출 - List likedUserIds = product.getLikeUserIds(); - if (!likedUserIds.isEmpty()) { - eventPublisher.publishEvent(NotificationEvent.createAuctionNotification(likedUserIds, AUCTION_START, - AUCTION_START.getMessage(product.getName()), - product.getFirstImageCdnPath(), auction.getId())); // 경매 시작 알림 - } - - log.info("경매가 시작되었습니다. 등록된 경매 마감 시간 : {}", auction.getEndDateTime()); - - return StartAuctionResponse.of( - auction.getId(), - auction.getProduct().getId(), - auction.getStatus(), - auction.getEndDateTime() - ); - } - - /** - * 경매 종료 처리 - */ - @Transactional - public void completeAuction(Long auctionId) { - log.info("경매 종료 작업 시작 auction ID: {}", auctionId); - Auction auction = getAuctionById(auctionId); - auction.endAuction(); - processAuctionResults(auction); - } - - /** - * 경매 ID로 경매 정보를 조회 - */ - private Auction getAuctionById(Long auctionId) { - return auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - } - - /** - * 사전 등록 상품 유효성 검사 - */ - private Product validateStartAuction(Long productId, Long userId) { - log.info("사전 등록 상품 유효성 검사를 시작합니다. 상품 ID: {}", productId); - Product product = productRepository.findById(productId) - .orElseThrow(() -> new ProductException(PRODUCT_NOT_FOUND)); - - // 등록된 상품의 사용자 정보와 전환 요청한 사용자 정보 유효성 검사 - if (!product.isOwner(userId)) { - throw new ProductException(FORBIDDEN_PRODUCT_ACCESS); - } - - // 이미 경매로 등록된 상품인지 유효성 검사 - if (auctionRepository.existsByProductId(product.getId())) { - throw new AuctionException(AUCTION_ALREADY_REGISTERED); - } - - log.info("유효성 검사가 끝났습니다. 상품 ID : {}", productId); - return product; - } - - /** - * 경매 결과 처리 - */ - private void processAuctionResults(Auction auction) { - Long productUserId = auction.getProduct().getUser().getId(); - String productName = auction.getProduct().getName(); - String firstImageCdnPath = auction.getProduct().getFirstImageCdnPath(); - List bids = bidService.findAllBidsByAuction(auction); - if (bids.isEmpty()) { // 입찰이 없는 경우 - eventPublisher.publishEvent( - NotificationEvent.createSimpleNotification(productUserId, AUCTION_FAILURE, - AUCTION_FAILURE.getMessage(productName), - firstImageCdnPath)); // 낙찰 실패 알림 이벤트 - return; - } - eventPublisher.publishEvent( - NotificationEvent.createAuctionNotification(productUserId, AUCTION_SUCCESS, - AUCTION_SUCCESS.getMessage(productName), - firstImageCdnPath, auction.getId())); // 낙찰 성공 알림 이벤트 - processWinningBid(auction, bids.get(0), productName, firstImageCdnPath); // 첫 번째 입찰이 낙찰 - processNonWinningBids(bids, productName, firstImageCdnPath); - - } - - /** - * 낙찰자 처리 - */ - private void processWinningBid(Auction auction, Bid winningBid, String productName, String firstImageCdnPath) { - auction.assignWinner(winningBid.getBidderId()); - eventPublisher.publishEvent( - NotificationEvent.createAuctionNotification(winningBid.getBidderId(), AUCTION_WINNER, - AUCTION_WINNER.getMessage(productName), firstImageCdnPath, auction.getId())); // 낙찰자 알림 이벤트 - log.info("경매 ID {}: 낙찰자 처리 완료", auction.getId()); - } - - /** - * 미낙찰자 처리 - */ - private void processNonWinningBids(List bids, String productName, String firstImageCdnPath) { - List nonWinnerIds = bids.stream().skip(1) // 낙찰자를 제외한 나머지 입찰자들 - .map(bid -> bid.getBidderId()).collect(Collectors.toList()); - - if (!nonWinnerIds.isEmpty()) { - eventPublisher.publishEvent(NotificationEvent.createSimpleNotification(nonWinnerIds, AUCTION_NON_WINNER, - AUCTION_NON_WINNER.getMessage(productName), firstImageCdnPath)); // 미낙찰자 알림 이벤트 - } - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionStartService.java similarity index 70% rename from src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionStartService.java index ebc09cee..ad290684 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionStartService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionStartService.java @@ -1,16 +1,16 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; import static org.chzz.market.domain.notification.entity.NotificationType.AUCTION_START; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.dto.AuctionRegistrationEvent; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.auction.dto.AuctionRegistrationEvent; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.like.repository.LikeRepository; import org.chzz.market.domain.notification.event.NotificationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -21,8 +21,8 @@ @Transactional(readOnly = true) @Slf4j public class AuctionStartService { - private final AuctionV2Repository auctionV2Repository; - private final LikeV2Repository likeRepository; + private final AuctionRepository auctionRepository; + private final LikeRepository likeRepository; private final ApplicationEventPublisher eventPublisher; /** @@ -30,7 +30,7 @@ public class AuctionStartService { */ @Transactional public void start(Long userId, Long auctionId) { - AuctionV2 auction = auctionV2Repository.findById(auctionId) + Auction auction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); auction.validateOwner(userId); auction.startOfficialAuction(); @@ -39,7 +39,7 @@ public void start(Long userId, Long auctionId) { log.info("{}번 경매 정식경매 전환완료", auctionId); } - private void processStartNotification(AuctionV2 auction) { + private void processStartNotification(Auction auction) { // 1. 해당 경매에 좋아요 누른 사용자 ID 추출 List likedUserIds = likeRepository.findByAuctionId(auction.getId()).stream().map(like -> like.getUserId()) .toList(); diff --git a/src/main/java/org/chzz/market/domain/auction/type/TestService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionTestService.java similarity index 71% rename from src/main/java/org/chzz/market/domain/auction/type/TestService.java rename to src/main/java/org/chzz/market/domain/auction/service/AuctionTestService.java index ea3bfa13..3ea1d86f 100644 --- a/src/main/java/org/chzz/market/domain/auction/type/TestService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionTestService.java @@ -1,31 +1,29 @@ -package org.chzz.market.domain.auction.type; +package org.chzz.market.domain.auction.service; import jakarta.transaction.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.Random; import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auction.dto.AuctionRegistrationEvent; import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.image.entity.Image; import org.chzz.market.domain.image.repository.ImageRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.product.repository.ProductRepository; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -/** - * 경매종료 테스트 서비스 삭제필요 - */ @Service @RequiredArgsConstructor -public class TestService { +public class AuctionTestService { private final AuctionRepository auctionRepository; - private final ProductRepository productRepository; private final ImageRepository imageRepository; private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public void test(Long userId, int seconds) { @@ -33,33 +31,31 @@ public void test(Long userId, int seconds) { int randomIndex = random.nextInt(1000) + 1; // 1부터 1000까지 랜덤 숫자 생성 int randomIndex1 = random.nextInt(1000) + 1; // 1부터 1000까지 랜덤 숫자 생성 User user = userRepository.findById(userId).get(); - Product product = Product.builder() + Auction auction = Auction.builder() .name("테스트" + randomIndex) .description("test") .category(Category.ELECTRONICS) - .user(user) + .seller(user) .minPrice(10000) + .status(AuctionStatus.PROCEEDING) + .endDateTime(LocalDateTime.now().plusSeconds(seconds)) .build(); - productRepository.save(product); + auctionRepository.save(auction); Image image1 = Image.builder() .cdnPath("https://picsum.photos/id/" + randomIndex + "/200/200") - .product(product) + .auction(auction) .sequence(1) .build(); Image image2 = Image.builder() .cdnPath("https://picsum.photos/id/" + randomIndex1 + "/200/200") - .product(product) + .auction(auction) .sequence(2) .build(); imageRepository.save(image1); imageRepository.save(image2); - product.addImages(List.of(image1, image2)); - auctionRepository.save(Auction.builder() - .status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusSeconds(seconds)) - .product(product) - .build()); + auction.addImages(List.of(image1, image2)); + eventPublisher.publishEvent(new AuctionRegistrationEvent(auction.getId(), auction.getEndDateTime())); } } diff --git a/src/main/java/org/chzz/market/domain/auction/service/AuctionWonService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionWonService.java new file mode 100644 index 00000000..6bd85397 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionWonService.java @@ -0,0 +1,36 @@ +package org.chzz.market.domain.auction.service; + +import static org.chzz.market.common.error.GlobalErrorCode.RESOURCE_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.NOT_WINNER; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.error.GlobalException; +import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuctionWonService { + private final AuctionRepository auctionRepository; + private final AuctionQueryRepository auctionQueryRepository; + + /** + * 낙찰 정보 조회 + */ + public WonAuctionDetailsResponse getWinningBidByAuctionId(Long userId, Long auctionId) { + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); + if (!auction.isWinner(userId)) { + throw new AuctionException(NOT_WINNER); + } + return auctionQueryRepository.findWinningBidById(auctionId) + .orElseThrow(() -> new GlobalException(RESOURCE_NOT_FOUND)); + } +} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java similarity index 70% rename from src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java rename to src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java index 6b4bae30..241fcdc2 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/PreAuctionRegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java @@ -1,12 +1,12 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import java.util.List; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; -import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.dto.ImageUploadEvent; +import org.chzz.market.domain.auction.dto.request.RegisterRequest; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.error.UserErrorCode; import org.chzz.market.domain.user.error.exception.UserException; @@ -19,7 +19,7 @@ @Service @RequiredArgsConstructor public class PreAuctionRegistrationService implements RegistrationService { - private final AuctionV2Repository auctionRepository; + private final AuctionRepository auctionRepository; private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; @@ -29,15 +29,15 @@ public void register(final Long userId, RegisterRequest request, final List new UserException(UserErrorCode.USER_NOT_FOUND)); - AuctionV2 auction = createAuction(request, user); + Auction auction = createAuction(request, user); auctionRepository.save(auction); eventPublisher.publishEvent(new ImageUploadEvent(auction, images)); } - private AuctionV2 createAuction(final RegisterRequest request, final User user) { - return AuctionV2.builder() + private Auction createAuction(final RegisterRequest request, final User user) { + return Auction.builder() .name(request.productName()) .minPrice(request.minPrice()) .category(request.category()) diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java similarity index 63% rename from src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java rename to src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java index 5f2ef194..d57bc2c7 100644 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/RegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java @@ -1,7 +1,7 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import java.util.List; -import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; +import org.chzz.market.domain.auction.dto.request.RegisterRequest; import org.springframework.web.multipart.MultipartFile; public interface RegistrationService { diff --git a/src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegisterService.java b/src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegisterService.java deleted file mode 100644 index 47154ff0..00000000 --- a/src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegisterService.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.chzz.market.domain.auction.service.register; - -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; - -import java.time.LocalDateTime; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.response.RegisterAuctionResponse; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.error.UserErrorCode; -import org.chzz.market.domain.user.error.exception.UserException; -import org.chzz.market.domain.user.repository.UserRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Service -@RequiredArgsConstructor -public class AuctionRegisterService implements AuctionRegistrationService { - private final UserRepository userRepository; - private final ProductRepository productRepository; - private final AuctionRepository auctionRepository; - private final ImageService imageService; - - @Override - @Transactional - public RegisterResponse register(Long userId, BaseRegisterRequest request, List images) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); - - Product product = createProduct(request, user); - List imageUrls = imageService.uploadImages(images); - List saveImages = imageService.saveProductImageEntities(imageUrls); - product.addImages(saveImages); - Product savedProduct = productRepository.save(product); - savedProduct.validateImageSize(); - - Auction auction = createAuction(savedProduct); - auctionRepository.save(auction); - - return RegisterAuctionResponse.of(savedProduct.getId(), auction.getId(), auction.getStatus()); - } - - private Product createProduct(BaseRegisterRequest request, User user) { - return Product.builder() - .user(user) - .name(request.getProductName()) - .minPrice(request.getMinPrice()) - .description(request.getDescription()) - .category(request.getCategory()) - .build(); - } - - private Auction createAuction(Product product) { - return Auction.builder() - .product(product) - .status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusHours(24)) - .build(); - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegistrationService.java deleted file mode 100644 index 9a406b62..00000000 --- a/src/main/java/org/chzz/market/domain/auction/service/register/AuctionRegistrationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.chzz.market.domain.auction.service.register; - -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; - -public interface AuctionRegistrationService { - RegisterResponse register(Long userId, BaseRegisterRequest request, List images); -} diff --git a/src/main/java/org/chzz/market/domain/auction/service/register/PreRegisterService.java b/src/main/java/org/chzz/market/domain/auction/service/register/PreRegisterService.java deleted file mode 100644 index 2839631e..00000000 --- a/src/main/java/org/chzz/market/domain/auction/service/register/PreRegisterService.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.chzz.market.domain.auction.service.register; - -import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.response.PreRegisterResponse; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.error.exception.UserException; -import org.chzz.market.domain.user.repository.UserRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Service -@RequiredArgsConstructor -public class PreRegisterService implements AuctionRegistrationService { - private final UserRepository userRepository; - private final ProductRepository productRepository; - private final ImageService imageService; - - @Override - @Transactional - public RegisterResponse register(Long userId, BaseRegisterRequest request, List images) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new UserException(USER_NOT_FOUND)); - Product product = createProduct(request, user); - List imageUrls = imageService.uploadImages(images); - List saveImages = imageService.saveProductImageEntities(imageUrls); - product.addImages(saveImages); - Product savedProduct = productRepository.save(product); - savedProduct.validateImageSize(); - return PreRegisterResponse.of(savedProduct.getId()); - } - - private Product createProduct(BaseRegisterRequest request, User user) { - return Product.builder() - .user(user) - .name(request.getProductName()) - .minPrice(request.getMinPrice()) - .description(request.getDescription()) - .category(request.getCategory()) - .build(); - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/type/AuctionRegisterType.java b/src/main/java/org/chzz/market/domain/auction/type/AuctionRegisterType.java deleted file mode 100644 index 18f85a9b..00000000 --- a/src/main/java/org/chzz/market/domain/auction/type/AuctionRegisterType.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.chzz.market.domain.auction.type; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum AuctionRegisterType { - PRE_REGISTER("사전 등록"), - REGISTER("경매 등록"); - - private final String description; -} diff --git a/src/main/java/org/chzz/market/domain/auction/type/AuctionStatus.java b/src/main/java/org/chzz/market/domain/auction/type/AuctionStatus.java deleted file mode 100644 index 7fa9eff8..00000000 --- a/src/main/java/org/chzz/market/domain/auction/type/AuctionStatus.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.chzz.market.domain.auction.type; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum AuctionStatus { - PROCEEDING("진행 중"), - ENDED("종료"); - - private final String description; -} diff --git a/src/main/java/org/chzz/market/domain/auction/type/AuctionViewType.java b/src/main/java/org/chzz/market/domain/auction/type/AuctionViewType.java deleted file mode 100644 index 481eee5b..00000000 --- a/src/main/java/org/chzz/market/domain/auction/type/AuctionViewType.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.chzz.market.domain.auction.type; - -public enum AuctionViewType { - FULL, - SIMPLE -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java deleted file mode 100644 index 09216498..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Api.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.chzz.market.domain.auctionv2.controller; - -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; -import static org.chzz.market.domain.user.error.UserErrorCode.Const.USER_NOT_FOUND; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Size; -import java.util.List; -import org.chzz.market.common.config.LoginUser; -import org.chzz.market.common.springdoc.ApiExceptionExplanation; -import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; -import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; -import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; -import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.user.error.UserErrorCode; -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.multipart.MultipartFile; - -@Tag(name = "auctions(v2)", description = "V2 경매 API") -@RequestMapping("/v2/auctions") -public interface AuctionV2Api { - @Operation(summary = "경매 목록 조회", description = "경매 목록을 조회합니다. status 파라미터를 통해 조회 유형을 지정합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "정식 경매 응답(페이징)", - content = {@Content( - mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = OfficialAuctionResponse.class)) - )} - ), - @ApiResponse(responseCode = "201", description = "사전 경매 응답(페이징)", - content = {@Content( - mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = PreAuctionResponse.class)) - )} - ) - }) - @ApiResponseExplanations( - errors = { - @ApiExceptionExplanation(value = AuctionErrorCode.class, constant = END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY, name = "minutes 파라미터는 진행중인 경매일 때만 사용가능"), - } - ) - @GetMapping - ResponseEntity> getAuctionList(@LoginUser Long userId, @RequestParam(required = false) Category category, - @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, - @RequestParam(required = false) @Min(value = 1, message = "minutes는 1 이상의 값이어야 합니다.") Integer minutes, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "경매 카테고리 조회", description = "경매 카테고리 목록을 조회합니다.") - @GetMapping("/categories") - ResponseEntity> getCategoryList(); - - @Operation(summary = "사용자가 등록한 진행중인 경매 목록 조회", description = "사용자가 등록한 진행중인 경매 목록을 조회합니다.") - @GetMapping("/users/proceeding") - ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "사용자가 등록한 종료된 경매 목록 조회", description = "사용자가 등록한 종료된 경매 목록을 조회합니다.") - @GetMapping("/users/ended") - ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "사용자가 등록한 사전 경매 목록 조회", description = "사용자가 등록한 사전 경매 목록을 조회합니다.") - @GetMapping("/users/pre") - ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "사용자가 낙찰한 경매 목록 조회", description = "사용자가 낙찰한 경매 목록을 조회합니다.") - @GetMapping("/users/won") - ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "사용자가 낙찰실패한 경매 목록 조회", description = "사용자가 낙찰실패한 경매 목록을 조회합니다.") - @GetMapping("/users/lost") - ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "사용자가 좋아요(찜)한 경매 목록 조회", description = "사용자가 좋아요(찜)한 경매 목록을 조회합니다.") - @GetMapping("/users/likes") - ResponseEntity> getLikedAuctionList(@LoginUser Long userId, - @ParameterObject @PageableDefault(sort = "newest-v2") Pageable pageable); - - @Operation(summary = "경매 등록", description = "경매를 등록합니다.") - @ApiResponseExplanations( - errors = { - @ApiExceptionExplanation(value = UserErrorCode.class, constant = USER_NOT_FOUND, name = "회원정보 조회 실패"), - } - ) - @PostMapping - ResponseEntity registerAuction(@LoginUser Long userId, - @RequestPart("request") @Valid RegisterRequest request, - @RequestPart(value = "images") @Valid - @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images); - - @Operation(summary = "경매 테스트 등록", description = "테스트 등록합니다.") - @PostMapping("/test") - ResponseEntity testEndAuction(@LoginUser Long userId, - @RequestParam int seconds); -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java b/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java deleted file mode 100644 index 6206223b..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/controller/AuctionV2Controller.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.chzz.market.domain.auctionv2.controller; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Size; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.chzz.market.common.config.LoginUser; -import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; -import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; -import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; -import org.chzz.market.domain.auctionv2.dto.response.CategoryResponse; -import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.service.AuctionCategoryService; -import org.chzz.market.domain.auctionv2.service.AuctionLookupService; -import org.chzz.market.domain.auctionv2.service.AuctionMyService; -import org.chzz.market.domain.auctionv2.service.AuctionTestService; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -@RestController -@RequiredArgsConstructor -@Validated -public class AuctionV2Controller implements AuctionV2Api { - private final AuctionLookupService auctionLookupService; - private final AuctionCategoryService auctionCategoryService; - private final AuctionTestService testService; - private final AuctionMyService auctionMyService; - - /** - * 경매 목록 조회 - */ - @Override - @GetMapping - public ResponseEntity> getAuctionList(@LoginUser Long userId, - @RequestParam(required = false) Category category, - @RequestParam(required = false, defaultValue = "proceeding") AuctionStatus status, - @RequestParam(required = false) @Min(value = 1, message = "minutes는 1 이상의 값이어야 합니다.") Integer minutes, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok( - auctionLookupService.getAuctionList(userId, category, status, minutes, pageable)); - } - - /** - * 경매 카테고리 Enum 조회 - */ - @Override - @GetMapping("/categories") - public ResponseEntity> getCategoryList() { - return ResponseEntity.ok(auctionCategoryService.getCategories()); - } - - /** - * 사용자가 등록한 진행중인 경매 목록 조회 - */ - @Override - @GetMapping("/users/proceeding") - public ResponseEntity> getUserProceedingAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionMyService.getUserProceedingAuctionList(userId, pageable)); - } - - /** - * 사용자가 등록한 종료된 경매 목록 조회 - */ - @Override - public ResponseEntity> getUserEndedAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionMyService.getUserEndedAuctionList(userId, pageable)); - } - - /** - * 사용자가 등록한 사전 경매 목록 조회 - */ - @Override - @GetMapping("/users/pre") - public ResponseEntity> getUserPreAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionMyService.getUserPreAuctionList(userId, pageable)); - } - - /** - * 사용자가 낙찰한 경매 목록 조회 - */ - @Override - public ResponseEntity> getUserWonAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionMyService.getUserWonAuctionList(userId, pageable)); - } - - /** - * 사용자가 낙찰실패한 경매 목록 조회 - */ - @Override - public ResponseEntity> getUserLostAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionMyService.getUserLostAuctionList(userId, pageable)); - } - - /** - * 사용자가 좋아요(찜)한 경매 목록 조회 - */ - @Override - @GetMapping("/users/likes") - public ResponseEntity> getLikedAuctionList(@LoginUser Long userId, - @PageableDefault(sort = "newest-v2") Pageable pageable) { - return ResponseEntity.ok(auctionMyService.getLikedAuctionList(userId, pageable)); - } - - /** - * 경매 등록 - */ - @Override - @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) - public ResponseEntity registerAuction(@LoginUser Long userId, - @RequestPart("request") @Valid RegisterRequest request, - @RequestPart(value = "images") @Valid - @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images) { - AuctionRegisterType type = request.auctionRegisterType(); - type.getService().register(userId, request, images);//요청 타입에 따라 다른 서비스 호출 - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - - /** - * 경매 테스트 등록 - */ - @Override - @PostMapping("/test") - public ResponseEntity testEndAuction(@LoginUser Long userId, - @RequestParam("seconds") int seconds) { - testService.test(userId, seconds); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java deleted file mode 100644 index 5561a80d..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/AuctionImageUpdateEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto; - -import java.util.Map; -import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.springframework.web.multipart.MultipartFile; - -public record AuctionImageUpdateEvent(AuctionV2 auction, - UpdateAuctionRequest request, - Map imageBuffer) { -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java deleted file mode 100644 index 97a8be5e..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/ImageUploadEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto; - -import java.util.List; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.springframework.web.multipart.MultipartFile; - -public record ImageUploadEvent(AuctionV2 auction, List images) { -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java deleted file mode 100644 index 8ef89bc5..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/ImageResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto.response; - -import org.chzz.market.domain.image.entity.ImageV2; - -public record ImageResponse( - Long imageId, - String imageUrl -) { - public static ImageResponse from(ImageV2 imageV2) { - return new ImageResponse(imageV2.getId(), imageV2.getCdnPath()); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java deleted file mode 100644 index ff7d8c7f..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/LostAuctionResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto.response; - -import java.time.LocalDateTime; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public class LostAuctionResponse extends BaseAuctionResponse { - private Long participantCount; - private LocalDateTime endDateTime; - private Long bidAmount; - - public LostAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, - Long participantCount, LocalDateTime endDateTime, Long bidAmount) { - super(auctionId, productName, imageUrl, minPrice, isSeller); - this.participantCount = participantCount; - this.endDateTime = endDateTime; - this.bidAmount = bidAmount; - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java deleted file mode 100644 index 7b78c445..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionDetailsResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto.response; - -import com.querydsl.core.annotations.QueryProjection; - -public record WonAuctionDetailsResponse( - Long auctionId, - String productName, - String imageUrl, - Long winningAmount -) { - @QueryProjection - public WonAuctionDetailsResponse { - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java b/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java deleted file mode 100644 index 181199fc..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/dto/response/WonAuctionResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.chzz.market.domain.auctionv2.dto.response; - -import java.time.LocalDateTime; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public class WonAuctionResponse extends BaseAuctionResponse { - private Long participantCount; - private LocalDateTime endDateTime; - private Long winningAmount; - private Boolean isOrdered; - private Long orderId; - - public WonAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, - Long participantCount, LocalDateTime endDateTime, Long winningAmount, Boolean isOrdered, - Long orderId) { - super(auctionId, productName, imageUrl, minPrice, isSeller); - this.participantCount = participantCount; - this.endDateTime = endDateTime; - this.winningAmount = winningAmount; - this.isOrdered = isOrdered; - this.orderId = orderId; - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java b/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java deleted file mode 100644 index 18f02d88..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/entity/AuctionV2.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.chzz.market.domain.auctionv2.entity; - -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.ENDED; -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PRE; -import static org.chzz.market.domain.auctionv2.entity.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ENDED; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_ENDED; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.base.entity.BaseTimeEntity; -import org.chzz.market.domain.image.entity.ImageV2; -import org.chzz.market.domain.imagev2.error.ImageErrorCode; -import org.chzz.market.domain.imagev2.error.exception.ImageException; -import org.chzz.market.domain.user.entity.User; -import org.hibernate.annotations.DynamicUpdate; - -// TODO: V2 경매 API 전환이 끝나서 운영 환경에 적용할 땐 기존 테이블에서 데이터를 이관해야 합니다.(flyway 스크립트) -@Table(name = "auction_v2") -@Entity -//@EntityListeners(value = AuctionEntityListener.class) -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Builder -@DynamicUpdate -@Getter -@Slf4j -public class AuctionV2 extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "auction_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "seller_id", nullable = false) - private User seller; - - @Column(nullable = false) - private String name; - - @Column(length = 1000) - private String description; - - @Column - private Integer minPrice; - - @Column(nullable = false, columnDefinition = "varchar(30)") - @Enumerated(EnumType.STRING) - private Category category; - - @Column - private LocalDateTime endDateTime; - - @Column(columnDefinition = "varchar(20)") - @Enumerated(EnumType.STRING) - private AuctionStatus status; - - @Column - private Long winnerId; - - @Builder.Default - @Column - private Long likeCount = 0L; - - @Builder.Default - @Column - private Long bidCount = 0L; - - @Builder.Default - @OneToMany(mappedBy = "auction", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval = true) - private List images = new ArrayList<>(); - - public void addImage(ImageV2 image) { - images.add(image); - image.specifyAuction(this); - } - - public boolean isOwner(Long userId) { - return seller.getId().equals(userId); - } - - public void validateOwner(Long userId) { - if (!isOwner(userId)) { - throw new AuctionException(AUCTION_ACCESS_FORBIDDEN); - } - } - - public boolean isPreAuction() { - return status == PRE; - } - - public boolean isOfficialAuction() { - return status == PROCEEDING || status == ENDED; - } - - public void validateAuctionEnded() { - if (!status.equals(ENDED)) { - throw new AuctionException(AUCTION_NOT_ENDED); - } - } - - public boolean isWinner(Long userId) { - return winnerId != null && winnerId.equals(userId); - } - - public void startOfficialAuction() { - if (isOfficialAuction()) { - throw new AuctionException(AUCTION_ALREADY_OFFICIAL); - } - this.status = PROCEEDING; - this.endDateTime = LocalDateTime.now().plusDays(1); - } - - public String getFirstImageCdnPath() { - return images.stream() - .filter(image -> image.getSequence() == 1) - .map(ImageV2::getCdnPath) - .findFirst() - .orElseThrow(() -> { - log.error("경매의 첫 번째 이미지가 없는 경우: {}", this.id); - return new ImageException(ImageErrorCode.IMAGE_NOT_FOUND); - }); - } - - public void validateAuctionEndTime() { - // 경매가 진행중이 아닐 때 - if (status != PROCEEDING || endDateTime == null || LocalDateTime.now().isAfter(endDateTime)) { - throw new AuctionException(AUCTION_ENDED); - } - } - - public boolean isAboveMinPrice(Long amount) { - return amount >= minPrice; - } - - public void addImages(final List images) { - this.images.addAll(images); - } - - public void endAuction() { - this.status = ENDED; - } - - public void assignWinner(final Long bidderId) { - this.winnerId = bidderId; - } - - public void update(final UpdateAuctionRequest request) { - this.name = request.getProductName(); - this.description = request.getDescription(); - this.category = request.getCategory(); - this.minPrice = request.getMinPrice(); - } - - public void validateImageSize() { - int count = this.images.size(); - if (count < 1) { - throw new AuctionException(AuctionErrorCode.NO_IMAGES_PROVIDED); - } else if (count > 5) { - throw new AuctionException(AuctionErrorCode.MAX_IMAGE_COUNT_EXCEEDED); - } - } - - public void removeImages(final List imagesToRemove) { - this.images.removeAll(imagesToRemove); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java deleted file mode 100644 index 7b0f1f20..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionErrorCode.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.chzz.market.domain.auctionv2.error; - -import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.FORBIDDEN; -import static org.springframework.http.HttpStatus.NOT_FOUND; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.chzz.market.common.error.ErrorCode; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum AuctionErrorCode implements ErrorCode { - AUCTION_NOT_ENDED(BAD_REQUEST, "해당 경매가 아직 끝나지 않았습니다."), - AUCTION_ALREADY_OFFICIAL(BAD_REQUEST, "해당 경매는 이미 정식 경매입니다."), - AUCTION_ENDED(BAD_REQUEST, "해당 경매가 진행 중이 아니거나 이미 종료되었습니다."), - END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY(BAD_REQUEST, - "진행중인 경매 목록 조회 시에만 minutes 파라미터를 사용할 수 있습니다."), - INVALID_IMAGE_COUNT(HttpStatus.BAD_REQUEST, "이미지 개수가 올바르지 않습니다."), - MAX_IMAGE_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지는 최대 5개까지 등록할 수 있습니다."), - NOT_A_PRE_AUCTION(BAD_REQUEST, "사전 등록 경매가 아닙니다"), - NO_IMAGES_PROVIDED(HttpStatus.BAD_REQUEST, "이미지가 제공되지 않았습니다."), - OFFICIAL_AUCTION_DELETE_FORBIDDEN(FORBIDDEN, "정식경매는 삭제할수 없습니다."), - NOW_WINNER(FORBIDDEN, "낙찰자가 아닙니다."), - AUCTION_ACCESS_FORBIDDEN(FORBIDDEN, "해당 경매에 접근할 수 없습니다."), - AUCTION_NOT_FOUND(NOT_FOUND, "경매를 찾을 수 없습니다."); - - private final HttpStatus httpStatus; - private final String message; - - public static class Const { - public static final String AUCTION_NOT_ENDED = "AUCTION_NOT_ENDED"; - public static final String AUCTION_ALREADY_OFFICIAL = "AUCTION_ALREADY_OFFICIAL"; - public static final String AUCTION_ENDED = "AUCTION_ENDED"; - public static final String END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY = "END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY"; - public static final String INVALID_IMAGE_COUNT = "INVALID_IMAGE_COUNT"; - public static final String OFFICIAL_AUCTION_DELETE_FORBIDDEN = "OFFICIAL_AUCTION_DELETE_FORBIDDEN"; - public static final String MAX_IMAGE_COUNT_EXCEEDED = "MAX_IMAGE_COUNT_EXCEEDED"; - public static final String NOT_A_PRE_AUCTION = "NOT_A_PRE_AUCTION"; - public static final String NO_IMAGES_PROVIDED = "NO_IMAGES_PROVIDED"; - public static final String NOW_WINNER = "NOW_WINNER"; - public static final String AUCTION_ACCESS_FORBIDDEN = "AUCTION_ACCESS_FORBIDDEN"; - public static final String AUCTION_NOT_FOUND = "AUCTION_NOT_FOUND"; - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionException.java b/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionException.java deleted file mode 100644 index 39893b6b..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/error/AuctionException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.chzz.market.domain.auctionv2.error; - -import org.chzz.market.common.error.ErrorCode; -import org.chzz.market.common.error.exception.BusinessException; - -public class AuctionException extends BusinessException { - public AuctionException(final ErrorCode errorCode) { - super(errorCode); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java b/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java deleted file mode 100644 index 2dd7d4ee..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/repository/AuctionV2Repository.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.chzz.market.domain.auctionv2.repository; - -import java.util.Optional; -import org.chzz.market.domain.auction.repository.AuctionRepositoryCustom; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; - -public interface AuctionV2Repository extends JpaRepository, AuctionRepositoryCustom { - @Query("SELECT a.status FROM AuctionV2 a WHERE a.id = :auctionId") - Optional findAuctionStatusById(Long auctionId); - - @Modifying - @Query("UPDATE AuctionV2 a SET a.likeCount = a.likeCount + 1 WHERE a.id = :auctionId") - void incrementLikeCount(Long auctionId); - - @Modifying - @Query("UPDATE AuctionV2 a SET a.likeCount = a.likeCount - 1 WHERE a.id = :auctionId AND a.likeCount > 0") - void decrementLikeCount(Long auctionId); - - @Modifying - @Query("UPDATE AuctionV2 a SET a.bidCount = a.bidCount + 1 WHERE a.id = :auctionId") - void incrementBidCount(Long auctionId); - - @Modifying - @Query("UPDATE AuctionV2 a SET a.bidCount = a.bidCount - 1 WHERE a.id = :auctionId AND a.bidCount > 0") - void decrementBidCount(Long auctionId); -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java b/src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java deleted file mode 100644 index 78aee803..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/schedule/AuctionV2EndJob.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.chzz.market.domain.auctionv2.schedule; - -import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.service.AuctionEndService; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.springframework.stereotype.Component; - -/** - * 경매 스케줄링 종료 작업 - */ -@Component -@RequiredArgsConstructor -public class AuctionV2EndJob implements Job { - private final AuctionEndService auctionEndService; - - @Override - public void execute(JobExecutionContext context) { - Long auctionId = context.getJobDetail().getJobDataMap().getLong("auctionId"); - auctionEndService.endAuction(auctionId); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java deleted file mode 100644 index 768aa022..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionTestService.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.chzz.market.domain.auctionv2.service; - -import jakarta.transaction.Transactional; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Random; -import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.repository.ImageRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.repository.UserRepository; -import org.springframework.stereotype.Service; - -/** - * 경매종료 테스트 서비스 삭제필요 - */ -@Service -@RequiredArgsConstructor -public class AuctionTestService { - private final AuctionRepository auctionRepository; - private final ProductRepository productRepository; - private final ImageRepository imageRepository; - private final UserRepository userRepository; - - @Transactional - public void test(Long userId, int seconds) { - Random random = new Random(); - int randomIndex = random.nextInt(1000) + 1; // 1부터 1000까지 랜덤 숫자 생성 - int randomIndex1 = random.nextInt(1000) + 1; // 1부터 1000까지 랜덤 숫자 생성 - User user = userRepository.findById(userId).get(); - Product product = Product.builder() - .name("테스트" + randomIndex) - .description("test") - .category(Category.ELECTRONICS) - .user(user) - .minPrice(10000) - .build(); - productRepository.save(product); - - Image image1 = Image.builder() - .cdnPath("https://picsum.photos/id/" + randomIndex + "/200/200") - .product(product) - .sequence(1) - .build(); - - Image image2 = Image.builder() - .cdnPath("https://picsum.photos/id/" + randomIndex1 + "/200/200") - .product(product) - .sequence(2) - .build(); - imageRepository.save(image1); - imageRepository.save(image2); - product.addImages(List.of(image1, image2)); - auctionRepository.save(Auction.builder() - .status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusSeconds(seconds)) - .product(product) - .build()); - } -} diff --git a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java b/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java deleted file mode 100644 index e843bbf0..00000000 --- a/src/main/java/org/chzz/market/domain/auctionv2/service/AuctionWonService.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.chzz.market.domain.auctionv2.service; - -import static org.chzz.market.common.error.GlobalErrorCode.RESOURCE_NOT_FOUND; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.NOW_WINNER; - -import lombok.RequiredArgsConstructor; -import org.chzz.market.common.error.GlobalException; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AuctionWonService { - private final AuctionV2Repository auctionRepository; - private final AuctionV2QueryRepository auctionQueryRepository; - - /** - * 낙찰 정보 조회 - */ - public WonAuctionDetailsResponse getWinningBidByAuctionId(Long userId, Long auctionId) { - AuctionV2 auction = auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - if (!auction.isWinner(userId)) { - throw new AuctionException(NOW_WINNER); - } - return auctionQueryRepository.findWinningBidById(auctionId) - .orElseThrow(() -> new GlobalException(RESOURCE_NOT_FOUND)); - } -} diff --git a/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java b/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java index 0842d60c..00fe1105 100644 --- a/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java +++ b/src/main/java/org/chzz/market/domain/bid/controller/BidApi.java @@ -1,7 +1,7 @@ package org.chzz.market.domain.bid.controller; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_ENDED; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_ENDED; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.Const.AUCTION_NOT_FOUND; import static org.chzz.market.domain.bid.error.BidErrorCode.Const.BID_ALREADY_CANCELLED; import static org.chzz.market.domain.bid.error.BidErrorCode.Const.BID_BELOW_MIN_PRICE; import static org.chzz.market.domain.bid.error.BidErrorCode.Const.BID_BY_OWNER; @@ -15,10 +15,10 @@ import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.bid.dto.BidCreateRequest; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.error.AuctionErrorCode; +import org.chzz.market.domain.bid.dto.request.BidCreateRequest; +import org.chzz.market.domain.bid.dto.response.BiddingRecord; import org.chzz.market.domain.bid.error.BidErrorCode; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; diff --git a/src/main/java/org/chzz/market/domain/bid/controller/BidController.java b/src/main/java/org/chzz/market/domain/bid/controller/BidController.java index 0a2e1e50..087c1e6d 100644 --- a/src/main/java/org/chzz/market/domain/bid/controller/BidController.java +++ b/src/main/java/org/chzz/market/domain/bid/controller/BidController.java @@ -5,9 +5,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.bid.dto.BidCreateRequest; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.bid.dto.request.BidCreateRequest; +import org.chzz.market.domain.bid.dto.response.BiddingRecord; import org.chzz.market.domain.bid.service.BidCancelService; import org.chzz.market.domain.bid.service.BidCreateService; import org.chzz.market.domain.bid.service.BidLookupService; diff --git a/src/main/java/org/chzz/market/domain/bid/dto/query/BiddingRecord.java b/src/main/java/org/chzz/market/domain/bid/dto/query/BiddingRecord.java deleted file mode 100644 index 45f9a6bc..00000000 --- a/src/main/java/org/chzz/market/domain/bid/dto/query/BiddingRecord.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.chzz.market.domain.bid.dto.query; - -import com.querydsl.core.annotations.QueryProjection; -import lombok.Getter; -import org.chzz.market.domain.auction.dto.BaseAuctionDto; -@Getter -public class BiddingRecord extends BaseAuctionDto { - private final Long auctionId; - private final Long bidAmount; - - @QueryProjection - public BiddingRecord(Long auctionId, String productName, Long minPrice, Long bidAmount, Long participantCount, String cdnPath, - Long timeRemaining) { - super(productName, cdnPath, timeRemaining, minPrice, participantCount); - this.auctionId = auctionId; - this.bidAmount = bidAmount; - } -} diff --git a/src/main/java/org/chzz/market/domain/bid/dto/BidCreateRequest.java b/src/main/java/org/chzz/market/domain/bid/dto/request/BidCreateRequest.java similarity index 94% rename from src/main/java/org/chzz/market/domain/bid/dto/BidCreateRequest.java rename to src/main/java/org/chzz/market/domain/bid/dto/request/BidCreateRequest.java index d62560ee..994d5795 100644 --- a/src/main/java/org/chzz/market/domain/bid/dto/BidCreateRequest.java +++ b/src/main/java/org/chzz/market/domain/bid/dto/request/BidCreateRequest.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.bid.dto; +package org.chzz.market.domain.bid.dto.request; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java b/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java new file mode 100644 index 00000000..483299d9 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java @@ -0,0 +1,20 @@ +package org.chzz.market.domain.bid.dto.response; + +import lombok.Getter; +import org.chzz.market.domain.auction.dto.response.BaseAuctionResponse; + +@Getter +public class BiddingRecord extends BaseAuctionResponse { + private final Long timeRemaining; + private final Long participantCount; + private final Long bidAmount; + + public BiddingRecord(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + Long timeRemaining, Long participantCount, Long bidAmount) { + super(auctionId, productName, imageUrl, minPrice, isSeller); + this.timeRemaining = timeRemaining; + this.participantCount = participantCount; + this.bidAmount = bidAmount; + } +} + diff --git a/src/main/java/org/chzz/market/domain/bid/entity/Bid.java b/src/main/java/org/chzz/market/domain/bid/entity/Bid.java index 27aa63af..b7f96d42 100644 --- a/src/main/java/org/chzz/market/domain/bid/entity/Bid.java +++ b/src/main/java/org/chzz/market/domain/bid/entity/Bid.java @@ -22,9 +22,6 @@ import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicUpdate; -/** - * bid 생성시 해당 경매에 입찰 기록을 통해 entity를 가져와 dirty checking 가능하도록 구현 예정 - */ @Entity @Getter @Table diff --git a/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java b/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java index 20790942..ffffbe2a 100644 --- a/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java +++ b/src/main/java/org/chzz/market/domain/bid/repository/BidQueryRepository.java @@ -2,25 +2,31 @@ import static com.querydsl.core.types.dsl.Expressions.numberTemplate; import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; -import static org.chzz.market.domain.auctionv2.entity.QAuctionV2.auctionV2; +import static org.chzz.market.domain.auction.entity.QAuction.auction; import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; import static org.chzz.market.domain.bid.entity.QBid.bid; -import static org.chzz.market.domain.image.entity.QImageV2.imageV2; +import static org.chzz.market.domain.image.entity.QImage.image; import static org.chzz.market.domain.user.entity.QUser.user; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.chzz.market.common.util.QuerydslOrder; import org.chzz.market.common.util.QuerydslOrderProvider; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; -import org.chzz.market.domain.bid.dto.query.QBiddingRecord; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.dto.response.BiddingRecord; import org.chzz.market.domain.bid.dto.response.QBidInfoResponse; import org.chzz.market.domain.bid.entity.Bid; import org.springframework.data.domain.Page; @@ -38,11 +44,11 @@ public class BidQueryRepository { * 특정 경매의 입찰 조회 */ public Page findBidsByAuctionId(Long auctionId, Pageable pageable) { - BooleanExpression isWinner = auctionV2.winnerId.isNotNull().and(auctionV2.winnerId.eq(user.id)); + BooleanExpression isWinner = auction.winnerId.isNotNull().and(auction.winnerId.eq(user.id)); JPAQuery baseQuery = jpaQueryFactory.from(bid) - .join(auctionV2).on(bid.auctionId.eq(auctionV2.id) - .and(auctionV2.id.eq(auctionId)) + .join(auction).on(bid.auctionId.eq(auction.id) + .and(auction.id.eq(auctionId)) .and(bid.status.eq(ACTIVE))); List content = baseQuery @@ -69,22 +75,24 @@ public Page findUsersBidHistory(Long userId, Pageable pageable, A // 공통된 부분을 baseQuery로 추출 JPAQuery baseQuery = jpaQueryFactory .from(bid) - .join(auctionV2).on(bid.auctionId.eq(auctionV2.id) + .join(auction).on(bid.auctionId.eq(auction.id) .and(bid.bidderId.eq(userId)) .and(bid.status.eq(ACTIVE)) .and(auctionStatusEqIgnoreNull(auctionStatus))); List result = baseQuery - .select(new QBiddingRecord( - auctionV2.id, - auctionV2.name, - auctionV2.minPrice.longValue(), - bid.amount, - auctionV2.bidCount, - imageV2.cdnPath, - timeRemaining().longValue() + .select(Projections.constructor( + BiddingRecord.class, + auction.id, + auction.name, + image.cdnPath, + auction.minPrice.longValue(), + Expressions.FALSE, + timeRemaining().longValue(), + auction.bidCount, + bid.amount )) - .leftJoin(imageV2).on(imageV2.auction.eq(auctionV2).and(isRepresentativeImage())) + .leftJoin(image).on(image.auction.eq(auction).and(isRepresentativeImage())) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -100,7 +108,7 @@ public Page findUsersBidHistory(Long userId, Pageable pageable, A /** * 특정 경매의 모든 입찰 Entity 조회 */ - public List findAllBidsByAuction(AuctionV2 auction) { + public List findAllBidsByAuction(Auction auction) { return jpaQueryFactory .selectFrom(bid) .where(bid.auctionId.eq(auction.getId()).and(bid.status.eq(ACTIVE))) @@ -109,15 +117,25 @@ public List findAllBidsByAuction(AuctionV2 auction) { } private BooleanExpression isRepresentativeImage() { - return imageV2.auction.eq(auctionV2).and(imageV2.sequence.eq(1)); + return image.auction.eq(auction).and(image.sequence.eq(1)); } private static NumberExpression timeRemaining() { return numberTemplate(Integer.class, - "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auctionV2.endDateTime); // 음수면 0으로 처리 + "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auction.endDateTime); // 음수면 0으로 처리 } private BooleanBuilder auctionStatusEqIgnoreNull(AuctionStatus status) { - return nullSafeBuilderIgnore(() -> auctionV2.status.eq(status)); + return nullSafeBuilderIgnore(() -> auction.status.eq(status)); + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public enum BidOrder implements QuerydslOrder { + AMOUNT("bid-amount", bid.amount.asc()), + TIME_REMAINING("time-remaining", auction.endDateTime.desc()); + + private final String name; + private final OrderSpecifier orderSpecifier; } } diff --git a/src/main/java/org/chzz/market/domain/bid/repository/BidRepository.java b/src/main/java/org/chzz/market/domain/bid/repository/BidRepository.java index 59b73745..3ba9b07b 100644 --- a/src/main/java/org/chzz/market/domain/bid/repository/BidRepository.java +++ b/src/main/java/org/chzz/market/domain/bid/repository/BidRepository.java @@ -4,6 +4,6 @@ import org.chzz.market.domain.bid.entity.Bid; import org.springframework.data.jpa.repository.JpaRepository; -public interface BidRepository extends JpaRepository, BidRepositoryCustom { +public interface BidRepository extends JpaRepository { Optional findByAuctionIdAndBidderId(Long auctionId, Long userId); } diff --git a/src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustom.java b/src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustom.java deleted file mode 100644 index dffd92fd..00000000 --- a/src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustom.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.chzz.market.domain.bid.repository; - -import java.util.List; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; -import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.bid.entity.Bid; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface BidRepositoryCustom { - Page findUsersBidHistory(Long userId, Pageable pageable, AuctionStatus status); - - List findAllBidsByAuction(Auction auction); - - Page findBidsByAuctionId(Long auctionId, Pageable pageable); -} diff --git a/src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImpl.java b/src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImpl.java deleted file mode 100644 index 2e6f27ac..00000000 --- a/src/main/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImpl.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.chzz.market.domain.bid.repository; - -import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; -import static org.chzz.market.domain.auction.entity.QAuction.auction; -import static org.chzz.market.domain.bid.entity.Bid.BidStatus.ACTIVE; -import static org.chzz.market.domain.bid.entity.QBid.bid; -import static org.chzz.market.domain.image.entity.QImage.image; -import static org.chzz.market.domain.product.entity.QProduct.product; -import static org.chzz.market.domain.user.entity.QUser.user; - -import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.core.types.dsl.NumberExpression; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.JPQLQuery; -import com.querydsl.jpa.impl.JPAQuery; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.chzz.market.common.util.QuerydslOrder; -import org.chzz.market.common.util.QuerydslOrderProvider; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; -import org.chzz.market.domain.bid.dto.query.QBiddingRecord; -import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.bid.dto.response.QBidInfoResponse; -import org.chzz.market.domain.bid.entity.Bid; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.support.PageableExecutionUtils; - -@RequiredArgsConstructor -public class BidRepositoryCustomImpl implements BidRepositoryCustom { - private final JPAQueryFactory jpaQueryFactory; - private final QuerydslOrderProvider querydslOrderProvider; - - public Page findUsersBidHistory(Long userId, Pageable pageable, AuctionStatus auctionStatus) { - // 공통된 부분을 baseQuery로 추출 - JPAQuery baseQuery = jpaQueryFactory - .from(bid) - .join(auction).on(bid.auctionId.eq(auction.id) - .and(bid.bidderId.eq(userId)) - .and(bid.status.eq(ACTIVE)) - .and(auctionStatusEqIgnoreNull(auctionStatus))); - - List result = baseQuery - .select(new QBiddingRecord( - auction.id, - product.name, - product.minPrice.longValue(), - bid.amount, - getBidCount(), - image.cdnPath, - timeRemaining().longValue() - )) - .leftJoin(auction.product, product) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - // 카운트 쿼리 작성 - JPAQuery countQuery = baseQuery - .select(bid.count()); - - return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); - } - - @Override - public List findAllBidsByAuction(Auction auction) { - return jpaQueryFactory - .selectFrom(bid) - .where(bid.auctionId.eq(auction.getId()).and(bid.status.eq(ACTIVE))) - .orderBy(bid.amount.desc(), bid.updatedAt.asc()) - .fetch(); - } - - @Override - public Page findBidsByAuctionId(Long auctionId, Pageable pageable) { - BooleanExpression isWinner = auction.winnerId.isNotNull().and(auction.winnerId.eq(user.id)) - .or(auction.winnerId.isNull().and(Expressions.FALSE)); - - JPAQuery baseQuery = jpaQueryFactory.from(bid) - .join(auction).on(bid.auctionId.eq(auction.id) - .and(auction.id.eq(auctionId)) - .and(bid.status.eq(ACTIVE))); - - List content = baseQuery - .select(new QBidInfoResponse( - bid.amount, - user.nickname, - isWinner - )) - .join(user).on(bid.bidderId.eq(user.id)) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery. - select(bid.count()); - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); - } - - /** - * 상품의 대표 이미지를 조회하기 위한 조건을 반환합니다. - * - * @return 대표 이미지(첫 번째 이미지)의 sequence가 1인 조건식 - */ - private BooleanExpression isRepresentativeImage() { - return image.sequence.eq(1); - } - - private JPQLQuery getBidCount() { - return JPAExpressions - .select(bid.count()) - .from(bid) - .where(bid.auctionId.eq(auction.id).and(bid.status.eq(ACTIVE))); - } - - private static NumberExpression timeRemaining() { - return Expressions.numberTemplate(Integer.class, - "GREATEST(0, TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP, {0}))", auction.endDateTime); // 음수면 0으로 처리 - } - - private BooleanBuilder auctionStatusEqIgnoreNull(AuctionStatus status) { - return nullSafeBuilderIgnore(() -> auction.status.eq(status)); - } - - @Getter - @AllArgsConstructor(access = AccessLevel.PRIVATE) - public enum BidOrder implements QuerydslOrder { - AMOUNT("bid-amount", bid.amount.asc()), - TIME_REMAINING("time-remaining", auction.endDateTime.desc()); - - private final String name; - private final OrderSpecifier orderSpecifier; - } -} diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java b/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java index def45325..e86647ae 100644 --- a/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java +++ b/src/main/java/org/chzz/market/domain/bid/service/BidCancelLockService.java @@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.chzz.market.common.aop.redisrock.DistributedLock; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.error.BidException; import org.chzz.market.domain.bid.repository.BidRepository; @@ -17,7 +17,7 @@ @Slf4j public class BidCancelLockService { - private final AuctionV2Repository auctionRepository; + private final AuctionRepository auctionRepository; private final BidRepository bidRepository; /** diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java b/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java index 17b31fb7..85414e8d 100644 --- a/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java +++ b/src/main/java/org/chzz/market/domain/bid/service/BidCreateService.java @@ -1,6 +1,6 @@ package org.chzz.market.domain.bid.service; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BELOW_MIN_PRICE; import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BY_OWNER; import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; @@ -8,10 +8,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.chzz.market.common.aop.redisrock.DistributedLock; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.bid.dto.BidCreateRequest; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.bid.dto.request.BidCreateRequest; import org.chzz.market.domain.bid.error.BidException; import org.chzz.market.domain.bid.repository.BidRepository; import org.chzz.market.domain.user.entity.User; @@ -25,7 +25,7 @@ @RequiredArgsConstructor @Slf4j public class BidCreateService { - private final AuctionV2Repository auctionRepository; + private final AuctionRepository auctionRepository; private final BidRepository bidRepository; private final UserRepository userRepository; @@ -33,7 +33,7 @@ public class BidCreateService { @DistributedLock(key = "'bid:' + #userId + ':' + #bidCreateRequest.auctionId") public void create(final BidCreateRequest bidCreateRequest, Long userId) { User user = userRepository.findById(userId).orElseThrow(() -> new UserException(USER_NOT_FOUND)); - AuctionV2 auction = auctionRepository.findById(bidCreateRequest.getAuctionId()) + Auction auction = auctionRepository.findById(bidCreateRequest.getAuctionId()) .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); validateBidConditions(bidCreateRequest, user.getId(), auction); bidRepository.findByAuctionIdAndBidderId(auction.getId(), userId) @@ -51,7 +51,7 @@ public void create(final BidCreateRequest bidCreateRequest, Long userId) { /** * 입찰 상태 유효성 검사 */ - private void validateBidConditions(BidCreateRequest bidCreateRequest, Long userId, AuctionV2 auction) { + private void validateBidConditions(BidCreateRequest bidCreateRequest, Long userId, Auction auction) { // 경매 등록자가 입찰할 때 if (auction.isOwner(userId)) { throw new BidException(BID_BY_OWNER); diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java b/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java index b9218eb7..1ea5407b 100644 --- a/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java +++ b/src/main/java/org/chzz/market/domain/bid/service/BidLookupService.java @@ -1,16 +1,16 @@ package org.chzz.market.domain.bid.service; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; import java.util.List; import lombok.RequiredArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; +import org.chzz.market.domain.bid.dto.response.BiddingRecord; import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.repository.BidQueryRepository; import org.springframework.data.domain.Page; @@ -22,14 +22,14 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class BidLookupService { - private final AuctionV2Repository auctionRepository; + private final AuctionRepository auctionRepository; private final BidQueryRepository bidQueryRepository; /** * 특정 경매의 모든 입찰 조회 */ public Page getBidsByAuctionId(Long userId, Long auctionId, Pageable pageable) { - AuctionV2 auction = auctionRepository.findById(auctionId) + Auction auction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); if (!auction.isOwner(userId)) { throw new AuctionException(AUCTION_ACCESS_FORBIDDEN); @@ -48,7 +48,7 @@ public Page inquireBidHistory(Long userId, Pageable pageable, Auc /** * 특정 경매의 입찰 Entity 조회 (경매 종료스케줄링에 사용) */ - public List findAllBidsByAuction(AuctionV2 auction) { + public List findAllBidsByAuction(Auction auction) { return bidQueryRepository.findAllBidsByAuction(auction); } } diff --git a/src/main/java/org/chzz/market/domain/bid/service/BidService.java b/src/main/java/org/chzz/market/domain/bid/service/BidService.java deleted file mode 100644 index 4f12c7e3..00000000 --- a/src/main/java/org/chzz/market/domain/bid/service/BidService.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.chzz.market.domain.bid.service; - -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.FORBIDDEN_AUCTION_ACCESS; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BELOW_MIN_PRICE; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BY_OWNER; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_NOT_ACCESSIBLE; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_NOT_FOUND; -import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.error.AuctionException; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.bid.dto.BidCreateRequest; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; -import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.bid.entity.Bid; -import org.chzz.market.domain.bid.error.BidException; -import org.chzz.market.domain.bid.repository.BidRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.error.exception.UserException; -import org.chzz.market.domain.user.repository.UserRepository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -@Slf4j -public class BidService { - private final AuctionRepository auctionRepository; - private final BidRepository bidRepository; - private final UserRepository userRepository; - - /** - * 나의 입찰 목록 조회 - */ - public Page inquireBidHistory(Long userId, Pageable pageable, AuctionStatus status) { - return bidRepository.findUsersBidHistory(userId, pageable, status); - } - - /** - * 특정 경매의 모든 입찰 조회 - */ - public List findAllBidsByAuction(Auction auction) { - return bidRepository.findAllBidsByAuction(auction); - } - - /** - * 종료된 특정 경매의 입찰 현황 조회 - */ - public Page getBidsByAuctionId(Long userId, Long auctionId, Pageable pageable) { - Auction auction = auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - if (!auction.getProduct().isOwner(userId)) { - throw new AuctionException(FORBIDDEN_AUCTION_ACCESS); - } - auction.validateAuctionEnded(); - return bidRepository.findBidsByAuctionId(auctionId, pageable); - } - - /** - * 입찰 완료 및 수정 - */ - @Transactional - public void createBid(final BidCreateRequest bidCreateRequest, Long userId) { - User user = userRepository.findById(userId).orElseThrow(() -> new UserException(USER_NOT_FOUND)); - Auction auction = auctionRepository.findById(bidCreateRequest.getAuctionId()) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - validateBidConditions(bidCreateRequest, user.getId(), auction); - bidRepository.findByAuctionIdAndBidderId(auction.getId(), userId) - .ifPresentOrElse( - // 이미 입찰을 한 경우 - bid -> bid.adjustBidAmount(bidCreateRequest.getBidAmount()), - // 입찰을 처음 하는 경우 - () -> bidRepository.save(bidCreateRequest.toEntity(user.getId())) - ); - } - - /** - * 입찰 취소 - */ - @Transactional - public void cancelBid(Long bidId, Long userId) { - Bid bid = bidRepository.findById(bidId).orElseThrow(() -> new BidException(BID_NOT_FOUND)); - Auction auction = auctionRepository.findById(bid.getAuctionId()) - .orElseThrow(() -> new AuctionException(AUCTION_NOT_FOUND)); - if (!bid.isOwner(userId)) { - throw new BidException(BID_NOT_ACCESSIBLE); - } - auction.validateAuctionEndTime(); - bid.cancelBid(); - log.info("입찰이 취소되었습니다. 입찰 ID: {}, 사용자 ID: {}, 경매 ID: {}", bid.getId(), userId, auction.getId()); - } - - /** - * 입찰 상태 유효성 검사 - */ - private void validateBidConditions(BidCreateRequest bidCreateRequest, Long userId, Auction auction) { - // 경매 등록자가 입찰할 때 - if (auction.getProduct().isOwner(userId)) { - throw new BidException(BID_BY_OWNER); - } - auction.validateAuctionEndTime(); - // 최소 금액보다 낮은 금액일 때 - if (!auction.isAboveMinPrice(bidCreateRequest.getBidAmount())) { - throw new BidException(BID_BELOW_MIN_PRICE); - } - } -} diff --git a/src/main/java/org/chzz/market/domain/image/dto/ImageResponse.java b/src/main/java/org/chzz/market/domain/image/dto/response/ImageResponse.java similarity index 87% rename from src/main/java/org/chzz/market/domain/image/dto/ImageResponse.java rename to src/main/java/org/chzz/market/domain/image/dto/response/ImageResponse.java index 1660e019..9817109e 100644 --- a/src/main/java/org/chzz/market/domain/image/dto/ImageResponse.java +++ b/src/main/java/org/chzz/market/domain/image/dto/response/ImageResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.image.dto; +package org.chzz.market.domain.image.dto.response; import com.querydsl.core.annotations.QueryProjection; import org.chzz.market.domain.image.entity.Image; @@ -7,7 +7,6 @@ public record ImageResponse( Long imageId, String imageUrl ) { - @QueryProjection public ImageResponse { } diff --git a/src/main/java/org/chzz/market/domain/image/entity/Image.java b/src/main/java/org/chzz/market/domain/image/entity/Image.java index 57a7edc5..61320253 100644 --- a/src/main/java/org/chzz/market/domain/image/entity/Image.java +++ b/src/main/java/org/chzz/market/domain/image/entity/Image.java @@ -15,16 +15,16 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.chzz.market.domain.auction.entity.Auction; import org.chzz.market.domain.base.entity.BaseTimeEntity; -import org.chzz.market.domain.product.entity.Product; @Getter -@Builder @Entity @Table(indexes = { - @Index(columnList = "product_id, image_id, cdn_path") + @Index(name = "idx_auction_image", columnList = "image_id, cdn_path, auction_id") }) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder @AllArgsConstructor public class Image extends BaseTimeEntity { @Id @@ -32,18 +32,18 @@ public class Image extends BaseTimeEntity { @Column(name = "image_id") private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id") + private Auction auction; + @Column(nullable = false) private String cdnPath; @Column private int sequence; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id") - private Product product; - - public void specifyProduct(Product product) { - this.product = product; + public void specifyAuction(Auction auction) { + this.auction = auction; } public void changeSequence(Integer newSequence) { diff --git a/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java b/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java index dd0f1765..adcba7d9 100644 --- a/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java +++ b/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java @@ -8,27 +8,18 @@ @Getter @AllArgsConstructor public enum ImageErrorCode implements ErrorCode { - IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드를 실패했습니다."), - IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다. "), - IMAGE_CONVERSION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 변환에 실패했습니다."), INVALID_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 확장자입니다."), - MAX_IMAGE_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지는 최대 5개까지 등록할 수 있습니다."), - INVALID_IMAGE_COUNT(HttpStatus.BAD_REQUEST, "이미지 개수가 올바르지 않습니다."), - NO_IMAGES_PROVIDED(HttpStatus.BAD_REQUEST, "이미지가 제공되지 않았습니다."), - NOT_FOUND(HttpStatus.BAD_REQUEST, "이미지가 없습니다."); - + IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지가 존재하지 않습니다."), + IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드를 실패했습니다."), + IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다."); private final HttpStatus httpStatus; private final String message; public static class Const { + public static final String INVALID_IMAGE_EXTENSION = "INVALID_IMAGE_EXTENSION"; + public static final String IMAGE_NOT_FOUND = "IMAGE_NOT_FOUND"; public static final String IMAGE_UPLOAD_FAILED = "IMAGE_UPLOAD_FAILED"; public static final String IMAGE_DELETE_FAILED = "IMAGE_DELETE_FAILED"; - public static final String IMAGE_CONVERSION_FAILURE = "IMAGE_CONVERSION_FAILURE"; - public static final String INVALID_IMAGE_EXTENSION = "INVALID_IMAGE_EXTENSION"; - public static final String MAX_IMAGE_COUNT_EXCEEDED = "MAX_IMAGE_COUNT_EXCEEDED"; - public static final String INVALID_IMAGE_COUNT = "INVALID_IMAGE_COUNT"; - public static final String NO_IMAGES_PROVIDED = "NO_IMAGES_PROVIDED"; - public static final String NOT_FOUND = "NOT_FOUND"; } } diff --git a/src/main/java/org/chzz/market/domain/imagev2/service/ImageDeleteService.java b/src/main/java/org/chzz/market/domain/image/service/ImageDeleteService.java similarity index 78% rename from src/main/java/org/chzz/market/domain/imagev2/service/ImageDeleteService.java rename to src/main/java/org/chzz/market/domain/image/service/ImageDeleteService.java index f2e04371..4b8f4b1d 100644 --- a/src/main/java/org/chzz/market/domain/imagev2/service/ImageDeleteService.java +++ b/src/main/java/org/chzz/market/domain/image/service/ImageDeleteService.java @@ -1,6 +1,6 @@ -package org.chzz.market.domain.imagev2.service; +package org.chzz.market.domain.image.service; -import static org.chzz.market.domain.imagev2.error.ImageErrorCode.IMAGE_DELETE_FAILED; +import static org.chzz.market.domain.image.error.ImageErrorCode.IMAGE_DELETE_FAILED; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; @@ -8,8 +8,8 @@ import java.net.URL; import java.util.List; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.image.entity.ImageV2; -import org.chzz.market.domain.imagev2.error.exception.ImageException; +import org.chzz.market.domain.image.entity.Image; +import org.chzz.market.domain.image.error.exception.ImageException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,9 +29,9 @@ public ImageDeleteService(AmazonS3 amazonS3Client, @Qualifier("s3BucketName") St /** * S3에서 이미지 삭제 */ - public void deleteImages(List images) { + public void deleteImages(List images) { images.stream() - .map(ImageV2::getCdnPath) + .map(Image::getCdnPath) .forEach(this::deleteImage); } diff --git a/src/main/java/org/chzz/market/domain/image/service/ImageService.java b/src/main/java/org/chzz/market/domain/image/service/ImageService.java index dbb34313..05ad95bd 100644 --- a/src/main/java/org/chzz/market/domain/image/service/ImageService.java +++ b/src/main/java/org/chzz/market/domain/image/service/ImageService.java @@ -1,157 +1,210 @@ package org.chzz.market.domain.image.service; -import static org.chzz.market.domain.image.error.ImageErrorCode.IMAGE_DELETE_FAILED; import static org.chzz.market.domain.image.error.ImageErrorCode.INVALID_IMAGE_EXTENSION; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.s3.AmazonS3; -import java.net.MalformedURLException; -import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auction.dto.AuctionImageUpdateEvent; +import org.chzz.market.domain.auction.dto.ImageUploadEvent; +import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionErrorCode; +import org.chzz.market.domain.auction.error.AuctionException; import org.chzz.market.domain.image.entity.Image; import org.chzz.market.domain.image.error.exception.ImageException; import org.chzz.market.domain.image.repository.ImageRepository; -import org.chzz.market.domain.product.entity.Product; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @RequiredArgsConstructor -@Transactional(readOnly = true) public class ImageService { - private final ImageUploader imageUploader; - private final ImageRepository imageRepository; - private final AmazonS3 amazonS3Client; + private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); @Value("${cloud.aws.cloudfront.domain}") private String cloudfrontDomain; - @Value("${cloud.aws.s3.bucket}") - private String bucket; + private final ImageRepository imageRepository; + private final S3ImageUploader s3ImageUploader; + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void uploadImages(final ImageUploadEvent event) { + Map buffer = setImageBuffer(event); + + List paths = s3ImageUploader.uploadImages(buffer); + + Auction auction = event.auction(); + + List list = createImages(auction, paths); + + auction.addImages(list); + + imageRepository.saveAll(list); + } + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void modifyImages(AuctionImageUpdateEvent event) { + Auction auction = event.auction(); + UpdateAuctionRequest request = event.request(); + Map buffer = event.imageBuffer(); + Map sequence = Optional.ofNullable(request.getImageSequence()).orElse(Collections.emptyMap()); + updateAuctionImages(auction, sequence, buffer); + } /** - * 여러 이미지 파일 업로드 및 CDN 경로 리스트 반환 + * 단일 파일 업로드 */ - public List uploadImages(List images) {//TODO 2024 10 07 23:18:22 : 이미지 벌크 업로드 방법 강구 - List uploadedUrls = images.stream() - .map(this::uploadImage) - .toList(); - log.info("업로드 된 이미지 리스트: {}", uploadedUrls); - return uploadedUrls; + public String uploadImage(MultipartFile file) { + String uniqueFileName = createUniqueFileName(file); + return cloudfrontDomain + "/" + s3ImageUploader.uploadImage(file, uniqueFileName); } /** - * 단일 이미지 파일 업로드 및 CDN 전체경로 리스트 반환 + * key - unique한 이미지 파일명
value - 해당 파일의 {@link MultipartFile} */ - //@Transactional(propagation = Propagation.NOT_SUPPORTED)// 써야하려나? 아니면 이벤트기반? - public String uploadImage(MultipartFile image) { - String uniqueFileName = createUniqueFileName(Objects.requireNonNull(image.getOriginalFilename())); - String s3Key = imageUploader.uploadImage(image, uniqueFileName); - return cloudfrontDomain + "/" + s3Key; + private Map setImageBuffer(final ImageUploadEvent event) { + Map imageBuffer = new HashMap<>(); + for (MultipartFile image : event.images()) { + String uniqueFileName = createUniqueFileName(image); + imageBuffer.put(uniqueFileName, image); + } + return imageBuffer; } /** - * 상품에 대한 이미지 Entity 생성 및 저장 + * @param paths 업로드된 이미지의 cdn 경로들 + * @return cdn과 순서를 적용한 {@link Image} list */ - @Transactional - public List saveProductImageEntities(List cdnPaths) { - List images = IntStream.range(0, cdnPaths.size()) + private List createImages(final Auction auction, final List paths) { + return IntStream.range(0, paths.size()) .mapToObj(i -> Image.builder() - .cdnPath(cdnPaths.get(i)) + .cdnPath(cloudfrontDomain + "/" + paths.get(i)) .sequence((i + 1)) + .auction(auction) .build()) .toList(); - return images; } /** - * 상품 수정 시 새로운 이미지 생성 및 저장 + * @param file 업로드한 파일 + * @return 원본파일명을 기반으로한 unique한 파일명 */ - @Transactional - public List uploadSequentialImages(Product product, Map newImages) { - List images = newImages.entrySet().stream() - .map(entry -> { - int sequence = Integer.parseInt(entry.getKey()); - MultipartFile multipartFile = entry.getValue(); - String cdnPath = uploadImage(multipartFile); - return Image.builder() - .sequence(sequence) - .cdnPath(cdnPath) - .product(product) - .build(); - }).toList(); - imageRepository.saveAll(images); - return images; + private String createUniqueFileName(MultipartFile file) { + String uuid = UUID.randomUUID().toString(); + String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); + + if (extension == null || !isValidFileExtension(extension)) { + throw new ImageException(INVALID_IMAGE_EXTENSION); + } + + return uuid + "." + extension; } /** - * 기존 이미지의 시퀀스를 업데이트하는 메서드 + * 파일 확장자 검증기
+ * + * @param extension 파일 확장자 */ - @Transactional - public void updateImageSequences(List imagesToUpdate, Map imageSequence) { - imagesToUpdate.forEach(image -> { - Long imageId = image.getId(); - Integer newSequence = imageSequence.get(imageId); - if (newSequence != null) { - image.changeSequence(newSequence); // 이미지의 시퀀스 업데이트 - } - }); + private boolean isValidFileExtension(String extension) { + return ALLOWED_EXTENSIONS.contains(extension.toLowerCase()); } /** - * 업로드된 이미지 삭제 + * 이미지 순서쌍을 포함한 변경요청({@link UpdateAuctionRequest})을 이용해 이미지 순서 변경 */ - public void deleteUploadImages(List fullImageUrls) { - fullImageUrls.forEach(this::deleteImage); + private void updateAuctionImages(final Auction auction, final Map sequence, + final Map multipartFileBuffer) { + validateTotalImageCount(sequence.size() + multipartFileBuffer.size()); + // 기존 이미지 처리 (업데이트할 이미지와 삭제할 이미지 구분) + processExistingImages(auction, sequence); + + // 새 이미지가 있는 경우 + if (!multipartFileBuffer.isEmpty()) { + uploadAndAddNewImages(auction, multipartFileBuffer); + } + auction.validateImageSize();// 업로드 이후 이미지 수량 검증 + } /** - * 단일 이미지 삭제 + * 요청의 순서 변경 요청과 새로운 이미지 갯수 총합을 통해 크기 검증 */ - private void deleteImage(String cdnPath) { - try { - URL url = new URL(cdnPath); - String path = url.getPath(); - String key = path.substring(1); - - log.info("S3에서 객체 삭제 시도, Key : {}", key); - amazonS3Client.deleteObject(bucket, key); - } catch (AmazonServiceException | MalformedURLException e) { - throw new ImageException(IMAGE_DELETE_FAILED); + private void validateTotalImageCount(int totalSize) { + if (totalSize > 5) { + throw new AuctionException(AuctionErrorCode.MAX_IMAGE_COUNT_EXCEEDED); + } else if (totalSize == 0) { + throw new AuctionException(AuctionErrorCode.INVALID_IMAGE_COUNT); } } /** - * 고유한 파일 이름 생성 + * 이미지 시퀀스 수정 및 이미지 삭제 */ - private String createUniqueFileName(String originalFileName) { - String uuid = UUID.randomUUID().toString(); - String extension = StringUtils.getFilenameExtension(originalFileName); - - if (extension == null || !isValidFileExtension(extension)) { - throw new ImageException(INVALID_IMAGE_EXTENSION); - } + private void processExistingImages(final Auction auction, final Map sequence) { + List imagesToUpdate = new ArrayList<>(); + List imagesToRemove = new ArrayList<>(); + + auction.getImages().forEach(image -> { + if (sequence.containsKey(image.getId())) { + imagesToUpdate.add(image); // 업데이트할 이미지 + } else { + imagesToRemove.add(image); // 삭제할 이미지 + } + }); + auction.removeImages(imagesToRemove); // 삭제할 이미지 처리 + updateImageSequences(imagesToUpdate, sequence); // 시퀀스 업데이트할 이미지 처리 + } - return uuid + "." + extension; + /** + * 이미지 순서 업데이트 + */ + private void updateImageSequences(final List imagesToUpdate, final Map sequence) { + imagesToUpdate.forEach(image -> { + Long imageId = image.getId(); + Integer newSequence = sequence.get(imageId); + if (newSequence != null) { + image.changeSequence(newSequence); // 이미지의 시퀀스 업데이트 + } + }); } /** - * 파일 확장자 검증 + * 이미지 업로드 및 영속화 */ - private boolean isValidFileExtension(String extension) { - List allowedExtensions = Arrays.asList("jpg", "jpeg", "png", "webp"); - return allowedExtensions.contains(extension.toLowerCase()); + private void uploadAndAddNewImages(final Auction auction, final Map multipartFileBuffer) { + List newImageEntities = uploadSequentialImages(auction, multipartFileBuffer); + auction.addImages(newImageEntities); + log.info("경매 ID {}번의 새 이미지를 성공적으로 저장하였습니다.", auction.getId()); } -} + private List uploadSequentialImages(Auction auction, Map newImages) { + List images = newImages.entrySet().stream() + .map(entry -> { + int sequence = Integer.parseInt(entry.getKey()); + MultipartFile multipartFile = entry.getValue(); + String uniqueFileName = createUniqueFileName(multipartFile); + String cdnPath = s3ImageUploader.uploadImage(multipartFile, uniqueFileName); + return Image.builder() + .sequence(sequence) + .cdnPath(cloudfrontDomain + "/" + cdnPath) + .auction(auction) + .build(); + }).toList(); + imageRepository.saveAll(images); + return images; + } +} diff --git a/src/main/java/org/chzz/market/domain/image/service/ImageUploader.java b/src/main/java/org/chzz/market/domain/image/service/ImageUploader.java deleted file mode 100644 index 63bbec5e..00000000 --- a/src/main/java/org/chzz/market/domain/image/service/ImageUploader.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.chzz.market.domain.image.service; - -import org.springframework.web.multipart.MultipartFile; - -/* - 테스트 이미지 업로드 인터페이스 - */ -public interface ImageUploader { - String uploadImage(MultipartFile image, String fileName); -} - diff --git a/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java b/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java index 1e67a1f5..b74baf9b 100644 --- a/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java +++ b/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java @@ -3,7 +3,6 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -17,15 +16,12 @@ @Slf4j @Service @RequiredArgsConstructor -public class S3ImageUploader implements ImageUploader { - private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); - +public class S3ImageUploader { private final AmazonS3 amazonS3Client; @Value("${cloud.aws.s3.bucket}") private String bucket; - @Override public String uploadImage(MultipartFile image, String fileName) { try { ObjectMetadata metadata = new ObjectMetadata(); @@ -45,23 +41,4 @@ public List uploadImages(final Map multipartFiles .map(entry -> uploadImage(entry.getValue(), entry.getKey())) .toList(); } - -// public List uploadImages(List images) { -// TransferManager transfer = TransferManagerBuilder.standard().withS3Client(amazonS3Client).build(); -// MultipleFileUpload upload = transfer.uploadFileList(bucket, "", new File("."), images); -// -// return upload.getSubTransfers().stream().map(this::getFileName).toList(); -// } -// -// /** -// * @return 각 파일의 key값(파일명) -// */ -// private String getFileName(final Upload subUpload) { -// try { -// UploadResult uploadResult = subUpload.waitForUploadResult(); -// return uploadResult.getKey(); -// } catch (InterruptedException e) { -// throw new ImageException(ImageErrorCode.IMAGE_UPLOAD_FAILED); -// } -// } } diff --git a/src/main/java/org/chzz/market/domain/imagev2/entity/ImageV2.java b/src/main/java/org/chzz/market/domain/imagev2/entity/ImageV2.java deleted file mode 100644 index 6401793e..00000000 --- a/src/main/java/org/chzz/market/domain/imagev2/entity/ImageV2.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.chzz.market.domain.image.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Index; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.base.entity.BaseTimeEntity; - -// TODO: V2 경매 API 전환이 끝나서 운영 환경에 적용할 땐 기존 테이블에서 데이터를 이관해야 합니다.(flyway 스크립트) -@Getter -@Entity -@Table(indexes = { - @Index(columnList = "auction_id, image_id, cdn_path") -}) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Builder -@AllArgsConstructor -public class ImageV2 extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "image_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "auction_id") - private AuctionV2 auction; - - @Column(nullable = false) - private String cdnPath; - - @Column - private int sequence; - - public void specifyAuction(AuctionV2 auction) { - this.auction = auction; - } - - public void changeSequence(Integer newSequence) { - this.sequence = newSequence; - } -} diff --git a/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java b/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java deleted file mode 100644 index cb882a96..00000000 --- a/src/main/java/org/chzz/market/domain/imagev2/error/ImageErrorCode.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.chzz.market.domain.imagev2.error; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.chzz.market.common.error.ErrorCode; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum ImageErrorCode implements ErrorCode { - IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다."), - IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지가 존재하지 않습니다."); - - private final HttpStatus httpStatus; - private final String message; - - public static class Const { - public static final String IMAGE_DELETE_FAILED = "IMAGE_DELETE_FAILED"; - public static final String IMAGE_NOT_FOUND = "IMAGE_NOT_FOUND"; - } -} diff --git a/src/main/java/org/chzz/market/domain/imagev2/error/exception/ImageException.java b/src/main/java/org/chzz/market/domain/imagev2/error/exception/ImageException.java deleted file mode 100644 index e99c63e8..00000000 --- a/src/main/java/org/chzz/market/domain/imagev2/error/exception/ImageException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.chzz.market.domain.imagev2.error.exception; - -import org.chzz.market.common.error.ErrorCode; -import org.chzz.market.common.error.exception.BusinessException; - -public class ImageException extends BusinessException { - public ImageException(final ErrorCode errorCode) { - super(errorCode); - } -} diff --git a/src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java b/src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java deleted file mode 100644 index e44c291e..00000000 --- a/src/main/java/org/chzz/market/domain/imagev2/repository/ImageV2Repository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.chzz.market.domain.imagev2.repository; - -import org.chzz.market.domain.image.entity.ImageV2; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ImageV2Repository extends JpaRepository { -} diff --git a/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java b/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java deleted file mode 100644 index a697ce37..00000000 --- a/src/main/java/org/chzz/market/domain/imagev2/service/ImageV2Service.java +++ /dev/null @@ -1,203 +0,0 @@ -package org.chzz.market.domain.imagev2.service; - -import static org.chzz.market.domain.image.error.ImageErrorCode.INVALID_IMAGE_EXTENSION; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.IntStream; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auctionv2.dto.AuctionImageUpdateEvent; -import org.chzz.market.domain.auctionv2.dto.ImageUploadEvent; -import org.chzz.market.domain.auctionv2.dto.request.UpdateAuctionRequest; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionErrorCode; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.image.entity.ImageV2; -import org.chzz.market.domain.image.error.exception.ImageException; -import org.chzz.market.domain.image.service.S3ImageUploader; -import org.chzz.market.domain.imagev2.repository.ImageV2Repository; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ImageV2Service { - private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); - - @Value("${cloud.aws.cloudfront.domain}") - private String cloudfrontDomain; - - private final ImageV2Repository imageRepository; - private final S3ImageUploader s3ImageUploader; - - @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) - public void uploadImages(final ImageUploadEvent event) { - Map buffer = setImageBuffer(event); - - List paths = s3ImageUploader.uploadImages(buffer); - - AuctionV2 auction = event.auction(); - - List list = createImages(auction, paths); - - auction.addImages(list); - - imageRepository.saveAll(list); - } - - @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) - public void modifyImages(AuctionImageUpdateEvent event) { - AuctionV2 auction = event.auction(); - UpdateAuctionRequest request = event.request(); - Map buffer = event.imageBuffer(); - Map sequence = Optional.ofNullable(request.getImageSequence()).orElse(Collections.emptyMap()); - updateAuctionImages(auction, sequence, buffer); - } - - /** - * key - unique한 이미지 파일명
value - 해당 파일의 {@link MultipartFile} - */ - private Map setImageBuffer(final ImageUploadEvent event) { - Map imageBuffer = new HashMap<>(); - for (MultipartFile image : event.images()) { - String uniqueFileName = createUniqueFileName(image); - imageBuffer.put(uniqueFileName, image); - } - return imageBuffer; - } - - /** - * @param paths 업로드된 이미지의 cdn 경로들 - * @return cdn과 순서를 적용한 {@link ImageV2} list - */ - private List createImages(final AuctionV2 auction, final List paths) { - return IntStream.range(0, paths.size()) - .mapToObj(i -> ImageV2.builder() - .cdnPath(cloudfrontDomain + "/" + paths.get(i)) - .sequence((i + 1)) - .auction(auction) - .build()) - .toList(); - } - - /** - * @param file 업로드한 파일 - * @return 원본파일명을 기반으로한 unique한 파일명 - */ - private String createUniqueFileName(MultipartFile file) { - String uuid = UUID.randomUUID().toString(); - String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); - - if (extension == null || !isValidFileExtension(extension)) { - throw new ImageException(INVALID_IMAGE_EXTENSION); - } - - return uuid + "." + extension; - } - - /** - * 파일 확장자 검증기
- * - * @param extension 파일 확장자 - */ - private boolean isValidFileExtension(String extension) { - return ALLOWED_EXTENSIONS.contains(extension.toLowerCase()); - } - - /** - * 이미지 순서쌍을 포함한 변경요청({@link UpdateAuctionRequest})을 이용해 이미지 순서 변경 - */ - private void updateAuctionImages(final AuctionV2 auction, final Map sequence, - final Map multipartFileBuffer) { - validateTotalImageCount(sequence.size() + multipartFileBuffer.size()); - // 기존 이미지 처리 (업데이트할 이미지와 삭제할 이미지 구분) - processExistingImages(auction, sequence); - - // 새 이미지가 있는 경우 - if (!multipartFileBuffer.isEmpty()) { - uploadAndAddNewImages(auction, multipartFileBuffer); - } - auction.validateImageSize();// 업로드 이후 이미지 수량 검증 - - } - - /** - * 요청의 순서 변경 요청과 새로운 이미지 갯수 총합을 통해 크기 검증 - */ - private void validateTotalImageCount(int totalSize) { - if (totalSize > 5) { - throw new AuctionException(AuctionErrorCode.MAX_IMAGE_COUNT_EXCEEDED); - } else if (totalSize == 0) { - throw new AuctionException(AuctionErrorCode.INVALID_IMAGE_COUNT); - } - } - - /** - * 이미지 시퀀스 수정 및 이미지 삭제 - */ - private void processExistingImages(final AuctionV2 auction, final Map sequence) { - List imagesToUpdate = new ArrayList<>(); - List imagesToRemove = new ArrayList<>(); - - auction.getImages().forEach(image -> { - if (sequence.containsKey(image.getId())) { - imagesToUpdate.add(image); // 업데이트할 이미지 - } else { - imagesToRemove.add(image); // 삭제할 이미지 - } - }); - auction.removeImages(imagesToRemove); // 삭제할 이미지 처리 - updateImageSequences(imagesToUpdate, sequence); // 시퀀스 업데이트할 이미지 처리 - } - - /** - * 이미지 순서 업데이트 - */ - private void updateImageSequences(final List imagesToUpdate, final Map sequence) { - imagesToUpdate.forEach(image -> { - Long imageId = image.getId(); - Integer newSequence = sequence.get(imageId); - if (newSequence != null) { - image.changeSequence(newSequence); // 이미지의 시퀀스 업데이트 - } - }); - } - - /** - * 이미지 업로드 및 영속화 - */ - private void uploadAndAddNewImages(final AuctionV2 auction, final Map multipartFileBuffer) { - List newImageEntities = uploadSequentialImages(auction, multipartFileBuffer); - auction.addImages(newImageEntities); - log.info("경매 ID {}번의 새 이미지를 성공적으로 저장하였습니다.", auction.getId()); - } - - private List uploadSequentialImages(AuctionV2 auction, Map newImages) { - List images = newImages.entrySet().stream() - .map(entry -> { - int sequence = Integer.parseInt(entry.getKey()); - MultipartFile multipartFile = entry.getValue(); - String uniqueFileName = createUniqueFileName(multipartFile); - String cdnPath = s3ImageUploader.uploadImage(multipartFile, uniqueFileName); - return ImageV2.builder() - .sequence(sequence) - .cdnPath(cloudfrontDomain + "/" + cdnPath) - .auction(auction) - .build(); - }).toList(); - imageRepository.saveAll(images); - return images; - } -} diff --git a/src/main/java/org/chzz/market/domain/like/dto/LikeResponse.java b/src/main/java/org/chzz/market/domain/like/dto/LikeResponse.java deleted file mode 100644 index 6aad07f7..00000000 --- a/src/main/java/org/chzz/market/domain/like/dto/LikeResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.chzz.market.domain.like.dto; - -public record LikeResponse (boolean isLiked, int likeCount) { - public static LikeResponse of(boolean isLiked, int likeCount) { - return new LikeResponse(isLiked, likeCount); - } -} diff --git a/src/main/java/org/chzz/market/domain/like/entity/Like.java b/src/main/java/org/chzz/market/domain/like/entity/Like.java index bd6c88fa..471ed243 100644 --- a/src/main/java/org/chzz/market/domain/like/entity/Like.java +++ b/src/main/java/org/chzz/market/domain/like/entity/Like.java @@ -2,39 +2,36 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.chzz.market.domain.base.entity.BaseTimeEntity; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.user.entity.User; @Getter @Entity @Builder @AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( - name = "like_table", - uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "product_id"})} + name = "likes", + uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "auction_id"})} ) -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Like extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "like_id") private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; + @Column(nullable = false) + private Long userId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id", nullable = false) - private Product product; + @Column(nullable = false) + private Long auctionId; } diff --git a/src/main/java/org/chzz/market/domain/like/error/LikeErrorCode.java b/src/main/java/org/chzz/market/domain/like/error/LikeErrorCode.java deleted file mode 100644 index 3163ce7e..00000000 --- a/src/main/java/org/chzz/market/domain/like/error/LikeErrorCode.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.chzz.market.domain.like.error; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.chzz.market.common.error.ErrorCode; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum LikeErrorCode implements ErrorCode { - LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "좋아요를 찾을 수 없습니다."); - - private final HttpStatus httpStatus; - private final String message; - - public static class Const { - public static final String LIKE_NOT_FOUND = "LIKE_NOT_FOUND"; - } -} diff --git a/src/main/java/org/chzz/market/domain/like/error/LikeException.java b/src/main/java/org/chzz/market/domain/like/error/LikeException.java deleted file mode 100644 index 5078993b..00000000 --- a/src/main/java/org/chzz/market/domain/like/error/LikeException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.chzz.market.domain.like.error; - -import org.chzz.market.common.error.ErrorCode; -import org.chzz.market.common.error.exception.BusinessException; - -public class LikeException extends BusinessException { - public LikeException(final ErrorCode errorCode) { - super(errorCode); - } -} diff --git a/src/main/java/org/chzz/market/domain/like/repository/LikeRepository.java b/src/main/java/org/chzz/market/domain/like/repository/LikeRepository.java index 6e6d875f..9a1a373f 100644 --- a/src/main/java/org/chzz/market/domain/like/repository/LikeRepository.java +++ b/src/main/java/org/chzz/market/domain/like/repository/LikeRepository.java @@ -1,13 +1,12 @@ package org.chzz.market.domain.like.repository; +import java.util.List; +import java.util.Optional; import org.chzz.market.domain.like.entity.Like; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface LikeRepository extends JpaRepository { - Optional findByUserAndProduct(User user, Product product); - boolean existsByUserIdAndProductId(Long userId, Long productId); -} \ No newline at end of file + List findByAuctionId(Long auctionId); + + Optional findByUserIdAndAuctionId(Long userId, Long auctionId); +} diff --git a/src/main/java/org/chzz/market/domain/like/service/LikeService.java b/src/main/java/org/chzz/market/domain/like/service/LikeService.java deleted file mode 100644 index 80deaa22..00000000 --- a/src/main/java/org/chzz/market/domain/like/service/LikeService.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.chzz.market.domain.like.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.like.dto.LikeResponse; -import org.chzz.market.domain.like.entity.Like; -import org.chzz.market.domain.like.error.LikeException; -import org.chzz.market.domain.like.repository.LikeRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.error.ProductException; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.error.exception.UserException; -import org.chzz.market.domain.user.repository.UserRepository; -import org.hibernate.dialect.lock.OptimisticEntityLockException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Retryable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import static org.chzz.market.domain.like.error.LikeErrorCode.LIKE_NOT_FOUND; -import static org.chzz.market.domain.product.error.ProductErrorCode.*; -import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -@Slf4j -public class LikeService { - - private final ProductRepository productRepository; - private final LikeRepository likeRepository; - private final UserRepository userRepository; - - @Transactional - @Retryable( - retryFor = {OptimisticEntityLockException.class}, - maxAttempts = 3, - backoff = @Backoff(delay = 1000) - ) - public LikeResponse toggleLike(Long userId, Long productId) { - log.info("상품 ID {}번 상품에 유저 ID {}번의 유저가 좋아요 토글 API를 호출합니다.", productId, userId); - Product product = findProductForLike(productId); - User user = findUser(userId); - - boolean isLiked = checkLikeExists(userId, productId); - - LikeResponse response; - if (isLiked) { - response = unlikeProduct(user, product); - log.info("유저 ID {}번의 유저가 상품 ID {}번의 상품 좋아요를 취소했습니다.", userId, productId); - } else { - response = likeProduct(user, product); - log.info("유저 ID {}번의 유저가 상품 ID {}번의 상품 좋아요를 눌렀습니다.", userId, productId); - } - - log.info("좋아요 토글 기능이 완료되었습니다. 좋아요: {}, 좋아요 개수: {}", response.isLiked(), product.getLikeCount()); - return response; - } - - private LikeResponse unlikeProduct(User user, Product product) { - Like like = likeRepository.findByUserAndProduct(user, product) - .orElseThrow(() -> new LikeException(LIKE_NOT_FOUND)); - product.removeLike(like); - likeRepository.delete(like); - return LikeResponse.of(false, product.getLikeCount()); - } - - private LikeResponse likeProduct(User user, Product product) { - Like newLike = Like.builder() - .user(user) - .product(product) - .build(); - product.addLike(newLike); - likeRepository.save(newLike); - return LikeResponse.of(true, product.getLikeCount()); - } - - private Product findProductForLike(Long productId) { - log.debug("상품 ID {}번의 상품 좋아요를 위한 상품 조회를 시작합니다.", productId); - return productRepository.findPreOrder(productId) - .orElseThrow(() -> new ProductException(PRODUCT_NOT_FOUND_OR_IN_AUCTION)); - } - - private User findUser(Long userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new UserException(USER_NOT_FOUND)); - } - - private boolean checkLikeExists(Long userId, Long productId) { - return likeRepository.existsByUserIdAndProductId(userId, productId); - } -} diff --git a/src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java b/src/main/java/org/chzz/market/domain/like/service/LikeUpdateService.java similarity index 67% rename from src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java rename to src/main/java/org/chzz/market/domain/like/service/LikeUpdateService.java index 50bef41d..3ee2cab7 100644 --- a/src/main/java/org/chzz/market/domain/likev2/service/LikeUpdateService.java +++ b/src/main/java/org/chzz/market/domain/like/service/LikeUpdateService.java @@ -1,21 +1,21 @@ -package org.chzz.market.domain.likev2.service; +package org.chzz.market.domain.like.service; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; import lombok.RequiredArgsConstructor; import org.chzz.market.common.aop.redisrock.DistributedLock; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.likev2.entity.LikeV2; -import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.like.entity.Like; +import org.chzz.market.domain.like.repository.LikeRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class LikeUpdateService { - private final AuctionV2Repository auctionRepository; - private final LikeV2Repository likeRepository; + private final AuctionRepository auctionRepository; + private final LikeRepository likeRepository; @DistributedLock(key = "'like:' + #userId + ':' + #auctionId") public void updateLike(Long userId, Long auctionId) { @@ -35,7 +35,7 @@ public void handleLikeTransaction(Long userId, Long auctionId) { ); } - private void handleUnlike(LikeV2 like, Long auctionId) { + private void handleUnlike(Like like, Long auctionId) { likeRepository.delete(like); auctionRepository.decrementLikeCount(auctionId); } @@ -45,8 +45,8 @@ private void handleLike(Long userId, Long auctionId) { auctionRepository.incrementLikeCount(auctionId); } - private LikeV2 createLike(Long userId, Long auctionId) { - return LikeV2.builder() + private Like createLike(Long userId, Long auctionId) { + return Like.builder() .userId(userId) .auctionId(auctionId) .build(); diff --git a/src/main/java/org/chzz/market/domain/likev2/entity/LikeV2.java b/src/main/java/org/chzz/market/domain/likev2/entity/LikeV2.java deleted file mode 100644 index 5f2c51f2..00000000 --- a/src/main/java/org/chzz/market/domain/likev2/entity/LikeV2.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.chzz.market.domain.likev2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.chzz.market.domain.base.entity.BaseTimeEntity; - -// TODO: V2 경매 API 전환이 끝나서 운영 환경에 적용할 땐 기존 테이블에서 데이터를 이관해야 합니다.(flyway 스크립트) -@Getter -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table( - name = "likes", - uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "auction_id"})} -) -public class LikeV2 extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "like_id") - private Long id; - - @Column(nullable = false) - private Long userId; - - @Column(nullable = false) - private Long auctionId; -} diff --git a/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java b/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java deleted file mode 100644 index 8af9ea55..00000000 --- a/src/main/java/org/chzz/market/domain/likev2/repository/LikeV2Repository.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.chzz.market.domain.likev2.repository; - -import java.util.List; -import java.util.Optional; -import org.chzz.market.domain.likev2.entity.LikeV2; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface LikeV2Repository extends JpaRepository { - List findByAuctionId(Long auctionId); - - Optional findByUserIdAndAuctionId(Long userId, Long auctionId); -} diff --git a/src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java b/src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java deleted file mode 100644 index bce00afe..00000000 --- a/src/main/java/org/chzz/market/domain/orderv2/entity/OrderV2.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.chzz.market.domain.orderv2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.paymentv2.entity.PaymentV2.PaymentMethod; - -@Entity -@Getter -@Builder -@Table(name = "orders_v2") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class OrderV2 { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "order_id") - private Long id; - - @Column(nullable = false) - private String orderNo; - - @Column(nullable = false) - private Long buyerId; - - @Column(nullable = false) - private Long paymentId; - - @Column(nullable = false) - private Long amount; - - @Column - private String deliveryMemo; - - @Column(nullable = false) - private String roadAddress; - - @Column(nullable = false) - private String jibun; - - @Column(nullable = false) - private String zipcode; - - @Column(nullable = false) - private String detailAddress; - - @Column(nullable = false) - private String recipientName; - - @Column(nullable = false) - private String phoneNumber; - - @Column(columnDefinition = "varchar(30)", nullable = false) - @Enumerated(EnumType.STRING) - private PaymentMethod method; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "auction_id") - private AuctionV2 auction; -} diff --git a/src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java b/src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java deleted file mode 100644 index 88d84c31..00000000 --- a/src/main/java/org/chzz/market/domain/orderv2/repository/OrderV2Repository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.chzz.market.domain.orderv2.repository; - -import org.chzz.market.domain.orderv2.entity.OrderV2; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface OrderV2Repository extends JpaRepository { -} diff --git a/src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java b/src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java deleted file mode 100644 index 1dacbe10..00000000 --- a/src/main/java/org/chzz/market/domain/paymentv2/entity/PaymentV2.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.chzz.market.domain.paymentv2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.base.entity.BaseTimeEntity; -import org.chzz.market.domain.user.entity.User; - -@Getter -@Entity -@Table -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PaymentV2 extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "payment_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User payer; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "auction_id", nullable = false) - private AuctionV2 auction; - - @Column(nullable = false) - private Long amount; - - @Column(columnDefinition = "varchar(30)", nullable = false) - @Enumerated(EnumType.STRING) - private PaymentMethod method; - - @Column(columnDefinition = "varchar(30)", nullable = false) - @Enumerated(EnumType.STRING) - private Status status; - - @Column(unique = true, nullable = false) - private String orderNo; - - @Column(nullable = false) - private String paymentKey; - - @PrePersist - protected void onPrePersist() { - if (this.status == null) { - this.status = Status.READY; - } - } - - @AllArgsConstructor - public enum PaymentMethod { - CARD("카드"), - VIRTUAL_ACCOUNT("가상계좌"), - EASY_PAYMENT("간편결제"), - MOBILE("휴대폰"), - ACCOUNT_TRANSFER("계좌이체"), - CULTURE_GIFT_CARD("문화상품권"), - BOOK_CULTURE_GIFT_CARD("도서문화상품권"), - GAME_CULTURE_GIFT_CARD("게임문화상품권"), - CASH("테스트용"); - - private final String description; - } -} diff --git a/src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java b/src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java deleted file mode 100644 index b17ea897..00000000 --- a/src/main/java/org/chzz/market/domain/paymentv2/entity/Status.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.chzz.market.domain.paymentv2.entity; - -public enum Status { - READY, - IN_PROGRESS, - WAITING_FOR_DEPOSIT, - DONE, - CANCELED, - PARTIAL_CANCELED, - ABORTED, - EXPIRED -} diff --git a/src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java b/src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java deleted file mode 100644 index 79dc0d34..00000000 --- a/src/main/java/org/chzz/market/domain/paymentv2/respository/PaymentV2Repository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.chzz.market.domain.paymentv2.respository; - -import org.chzz.market.domain.paymentv2.entity.PaymentV2; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PaymentV2Repository extends JpaRepository { -} diff --git a/src/main/java/org/chzz/market/domain/product/controller/ProductApi.java b/src/main/java/org/chzz/market/domain/product/controller/ProductApi.java deleted file mode 100644 index 96894730..00000000 --- a/src/main/java/org/chzz/market/domain/product/controller/ProductApi.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.chzz.market.domain.product.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import java.util.List; -import java.util.Map; -import org.chzz.market.domain.like.dto.LikeResponse; -import org.chzz.market.domain.product.dto.CategoryResponse; -import org.chzz.market.domain.product.dto.DeleteProductResponse; -import org.chzz.market.domain.product.dto.ProductDetailsResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.dto.UpdateProductResponse; -import org.chzz.market.domain.product.entity.Product.Category; -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.multipart.MultipartFile; - -@Tag(name = "products", description = "사전 경매 API") -public interface ProductApi { - - @Operation(summary = "사전 경매 목록 조회") - ResponseEntity> getProductList(Category category, Long userId, - @ParameterObject Pageable pageable); - - @Operation(summary = "상품 카테고리 목록 조회") - ResponseEntity> getCategoryList(); - - @Operation(summary = "사전 경매 상세 조회") - ResponseEntity getProductDetails(Long productId, Long userId); - - @Operation(summary = "특정 닉네임 사용자의 사전 경매 목록 조회 (현재 사용 x)") - ResponseEntity> getMyProductList(String nickname, @ParameterObject Pageable pageable); - - @Operation(summary = "나의 사전 경매 목록 조회") - ResponseEntity> getRegisteredProductList(Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "나의 좋아요 사전 경매 목록 조회") - ResponseEntity> getLikedProductList(Long userId, @ParameterObject Pageable pageable); - - @Operation(summary = "사전 경매 수정") - @Parameter( - name = "sequence (예: 1)", - description = "key: 이미지 순서(1~5), value: 업로드할 이미지 파일", - schema = @Schema(type = "string", format = "binary") - ) - ResponseEntity updateProduct(Long userId, Long productId, @Valid UpdateProductRequest request, - Map images); - - @Operation(summary = "사전 경매 삭제") - ResponseEntity deleteProduct(Long productId, Long userId); - - @Operation(summary = "좋아요 요청 및 취소") - ResponseEntity toggleProductLike(Long productId, Long userId); -} diff --git a/src/main/java/org/chzz/market/domain/product/controller/ProductController.java b/src/main/java/org/chzz/market/domain/product/controller/ProductController.java deleted file mode 100644 index 50b5bb54..00000000 --- a/src/main/java/org/chzz/market/domain/product/controller/ProductController.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.chzz.market.domain.product.controller; - -import static org.chzz.market.domain.product.entity.Product.Category; - -import jakarta.validation.Valid; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.common.config.LoginUser; -import org.chzz.market.domain.like.dto.LikeResponse; -import org.chzz.market.domain.like.service.LikeService; -import org.chzz.market.domain.product.dto.CategoryResponse; -import org.chzz.market.domain.product.dto.DeleteProductResponse; -import org.chzz.market.domain.product.dto.ProductDetailsResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.dto.UpdateProductResponse; -import org.chzz.market.domain.product.service.ProductService; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - - -@Slf4j -@RequiredArgsConstructor -@RestController -@RequestMapping("/v1/products") -public class ProductController implements ProductApi { - private final ProductService productService; - private final LikeService likeService; - - /** - * 사전 등록 상품 목록 조회 - */ - @Override - @GetMapping - public ResponseEntity> getProductList( - @RequestParam(required = false) Category category, - @LoginUser Long userId, - Pageable pageable) { - return ResponseEntity.ok(productService.getProductListByCategory(category, userId, pageable)); - } - - /** - * 상품 카테고리 목록 조회 - */ - @Override - @GetMapping("/categories") - public ResponseEntity> getCategoryList() { - return ResponseEntity.ok(productService.getCategories()); - } - - /** - * 사전 등록 상품 상세 정보 조회 - */ - @Override - @GetMapping("/{productId}") - public ResponseEntity getProductDetails( - @PathVariable Long productId, - @LoginUser Long userId) { - ProductDetailsResponse response = productService.getProductDetails(productId, userId); - return ResponseEntity.ok(response); - } - - /** - * 특정 닉네임 사용자의 사전 경매 목록 조회 (현재 사용 x) - */ - @Override - @GetMapping("/users/{nickname}") - public ResponseEntity> getMyProductList( - @PathVariable String nickname, - Pageable pageable) { - return ResponseEntity.ok(productService.getProductListByNickname(nickname, pageable)); - } - - /** - * 나의 사전 경매 목록 조회 - */ - @Override - @GetMapping("/users") - public ResponseEntity> getRegisteredProductList( - @LoginUser Long userId, - Pageable pageable) { - return ResponseEntity.ok(productService.getProductListByUserId(userId, pageable)); - } - - /** - * 내가 참여한 사전경매 조회 - */ - @Override - @GetMapping("/history") - public ResponseEntity> getLikedProductList( - @LoginUser Long userId, - @PageableDefault(size = 20, sort = "product-newest") Pageable pageable) { - return ResponseEntity.ok(productService.getLikedProductList(userId, pageable)); - } - - /** - * 사전 등록 상품 수정 - */ - @Override - @PatchMapping(value = "/{productId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity updateProduct( - @LoginUser Long userId, - @PathVariable Long productId, - @RequestPart("request") @Valid UpdateProductRequest request, - @RequestParam(required = false) Map images) { - UpdateProductResponse response = productService.updateProduct(userId, productId, request, images); - return ResponseEntity.status(HttpStatus.OK).body(response); - } - - /** - * 사전 등록 상품 삭제 - */ - @Override - @DeleteMapping("/{productId}") - public ResponseEntity deleteProduct( - @PathVariable Long productId, - @LoginUser Long userId) { - DeleteProductResponse response = productService.deleteProduct(productId, userId); - log.info("상품이 성공적으로 삭제되었습니다. 상품 ID: {}", productId); - return ResponseEntity.status(HttpStatus.OK).body(response); - } - - /** - * 상품 좋아요 토글 - */ - @Override - @PostMapping("/{productId}/likes") - public ResponseEntity toggleProductLike( - @PathVariable Long productId, - @LoginUser Long userId) { - LikeResponse response = likeService.toggleLike(userId, productId); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/org/chzz/market/domain/product/dto/BaseProductDto.java b/src/main/java/org/chzz/market/domain/product/dto/BaseProductDto.java deleted file mode 100644 index 707067b4..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/BaseProductDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.chzz.market.domain.product.dto; - -import lombok.Getter; -import lombok.ToString; - -@Getter -@ToString -public abstract class BaseProductDto { - protected String productName; - protected String imageUrl; - protected Long likeCount; - protected Integer minPrice; - - public BaseProductDto(String productName, String imageUrl, Long likeCount, Integer minPrice) { - this.productName = productName; - this.imageUrl = imageUrl; - this.likeCount = likeCount; - this.minPrice = minPrice; - } -} diff --git a/src/main/java/org/chzz/market/domain/product/dto/CategoryResponse.java b/src/main/java/org/chzz/market/domain/product/dto/CategoryResponse.java deleted file mode 100644 index fe071c63..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/CategoryResponse.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.chzz.market.domain.product.dto; - -public record CategoryResponse (String code, String displayName) {} diff --git a/src/main/java/org/chzz/market/domain/product/dto/DeleteProductResponse.java b/src/main/java/org/chzz/market/domain/product/dto/DeleteProductResponse.java deleted file mode 100644 index dfef8a4f..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/DeleteProductResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.chzz.market.domain.product.dto; - -import org.chzz.market.domain.product.entity.Product; - -public record DeleteProductResponse ( - Long productId, - String productName, - int likeCount, - String message -) { - private static final String DELETE_SUCCESS_MESSAGE = "사전 등록 상품이 성공적으로 삭제되었습니다. 상품 ID: %d, 좋아요 누름 사용자 수: %d"; - - public static DeleteProductResponse ofPreRegistered(Product product, int likeCount) { - return new DeleteProductResponse( - product.getId(), - product.getName(), - likeCount, - String.format(DELETE_SUCCESS_MESSAGE, product.getId(), likeCount) - ); - } -} diff --git a/src/main/java/org/chzz/market/domain/product/dto/ProductDetailsResponse.java b/src/main/java/org/chzz/market/domain/product/dto/ProductDetailsResponse.java deleted file mode 100644 index c30c8544..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/ProductDetailsResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.chzz.market.domain.product.dto; - -import com.querydsl.core.annotations.QueryProjection; -import java.time.LocalDateTime; -import java.util.List; -import lombok.Getter; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.product.entity.Product.Category; - -/** - * 사전 등록 상품 상세 조회 DTO - */ -@Getter -public class ProductDetailsResponse { - private final Long productId; - private final String productName; - private final String sellerNickname; - private final String sellerProfileImageUrl; - private final Integer minPrice; - private final LocalDateTime updatedAt; - private final String description; - private final Long likeCount; - private final Boolean isLiked; - private final Boolean isSeller; - private final Category category; - private List images; - - @QueryProjection - public ProductDetailsResponse(Long productId, String productName, String sellerNickname, - String sellerProfileImageUrl, - Integer minPrice, LocalDateTime updatedAt, String description, - Long likeCount, Boolean isLiked, Boolean isSeller, Category category) { - this.productId = productId; - this.productName = productName; - this.sellerNickname = sellerNickname; - this.sellerProfileImageUrl = sellerProfileImageUrl; - this.minPrice = minPrice; - this.updatedAt = updatedAt; - this.description = description; - this.likeCount = likeCount; - this.isLiked = isLiked; - this.isSeller = isSeller; - this.category = category; - } - - public void addImageList(List images) { - this.images = images; - } -} diff --git a/src/main/java/org/chzz/market/domain/product/dto/ProductResponse.java b/src/main/java/org/chzz/market/domain/product/dto/ProductResponse.java deleted file mode 100644 index 238e2cdf..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/ProductResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.chzz.market.domain.product.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.querydsl.core.annotations.QueryProjection; -import lombok.Getter; - - -/** - * 사전 등록 상품 목록 조회 DTO - */ -@Getter -public class ProductResponse extends BaseProductDto { - private final Long productId; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Boolean isLiked; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Boolean isSeller; - - @QueryProjection - public ProductResponse(Long productId, String name, String cdnPath, Integer minPrice, - Long likeCount, Boolean isLiked, Boolean isSeller) { - super(name, cdnPath, likeCount, minPrice); - this.productId = productId; - this.isLiked = isLiked; - this.isSeller = isSeller; - } - - @QueryProjection - public ProductResponse(Long productId, String name, String cdnPath, Integer minPrice, - Long likeCount, Boolean isLiked) { - super(name, cdnPath, likeCount, minPrice); - this.productId = productId; - this.isLiked = isLiked; - } - - @QueryProjection - public ProductResponse(Long productId, String name, String cdnPath, Integer minPrice, - Long likeCount) { - super(name, cdnPath, likeCount, minPrice); - this.productId = productId; - } -} diff --git a/src/main/java/org/chzz/market/domain/product/dto/UpdateProductRequest.java b/src/main/java/org/chzz/market/domain/product/dto/UpdateProductRequest.java deleted file mode 100644 index 43f54328..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/UpdateProductRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.chzz.market.domain.product.dto; - -import static org.chzz.market.domain.auction.dto.request.BaseRegisterRequest.DESCRIPTION_REGEX; -import static org.chzz.market.domain.product.entity.Product.Category; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import java.util.HashMap; -import java.util.Map; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.chzz.market.common.validation.annotation.ThousandMultiple; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UpdateProductRequest { - @Size(min = 2, max = 30, message = "제목은 최소 2글자 이상 30자 이하여야 합니다") - private String productName; - - @Schema(description = "개행문자 포함 최대 1000자, 개행문자 최대 10개") - @Size(max = 1000, message = "상품설명은 1000자 이내여야 합니다.") - @Pattern(regexp = DESCRIPTION_REGEX, message = "줄 바꿈 10번까지 가능합니다") - protected String description; - - private Category category; - - @ThousandMultiple - @Max(value = 2_000_000, message = "최소금액은 200만원을 넘을 수 없습니다") - private Integer minPrice; - - @Builder.Default - private Map imageSequence = new HashMap<>(); -} diff --git a/src/main/java/org/chzz/market/domain/product/dto/UpdateProductResponse.java b/src/main/java/org/chzz/market/domain/product/dto/UpdateProductResponse.java deleted file mode 100644 index 2b6d5fb0..00000000 --- a/src/main/java/org/chzz/market/domain/product/dto/UpdateProductResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.chzz.market.domain.product.dto; - -import java.util.List; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; - -public record UpdateProductResponse( - Long productId, - String productName, - String description, - Category category, - Integer minPrice, - List imageUrls -) { - public static UpdateProductResponse from(Product product) { - return new UpdateProductResponse( - product.getId(), - product.getName(), - product.getDescription(), - product.getCategory(), - product.getMinPrice(), - product.getImages().stream() - .map(ImageResponse::from) - .toList() - ); - } -} diff --git a/src/main/java/org/chzz/market/domain/product/entity/Product.java b/src/main/java/org/chzz/market/domain/product/entity/Product.java deleted file mode 100644 index 91558041..00000000 --- a/src/main/java/org/chzz/market/domain/product/entity/Product.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.chzz.market.domain.product.entity; - -import static org.chzz.market.domain.image.error.ImageErrorCode.MAX_IMAGE_COUNT_EXCEEDED; -import static org.chzz.market.domain.image.error.ImageErrorCode.NO_IMAGES_PROVIDED; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Index; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import java.util.ArrayList; -import java.util.List; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.chzz.market.domain.base.entity.BaseTimeEntity; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.error.exception.ImageException; -import org.chzz.market.domain.like.entity.Like; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.error.ProductErrorCode; -import org.chzz.market.domain.product.error.ProductException; -import org.chzz.market.domain.user.entity.User; -import org.hibernate.annotations.DynamicUpdate; - -@Getter -@Entity -@Builder -@Table(indexes = { - @Index(name = "idx_product_id_name", columnList = "product_id, name") -}) -@DynamicUpdate -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Product extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "product_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @Column(nullable = false) - private String name; - - @Column(length = 1000) - private String description; - - @Column - private Integer minPrice; - - @Column(nullable = false, columnDefinition = "varchar(30)") - @Enumerated(EnumType.STRING) - private Category category; - - @Builder.Default - @OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - @Builder.Default - @OneToMany(mappedBy = "product", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval = true) - private List images = new ArrayList<>(); - - @Getter - @AllArgsConstructor - public enum Category { - ELECTRONICS("전자기기"), - HOME_APPLIANCES("가전제품"), - FASHION_AND_CLOTHING("패션 및 의류"), - FURNITURE_AND_INTERIOR("가구 및 인테리어"), - BOOKS_AND_MEDIA("도서 및 미디어"), - SPORTS_AND_LEISURE("스포츠 및 레저"), - TOYS_AND_HOBBIES("장난감 및 취미"), - OTHER("기타"); - - private final String displayName; - } - - // 좋아요 수 계산 메서드 - public int getLikeCount() { - return likes.size(); - } - - // 좋아요 추가 메서드 - public void addLike(Like like) { - likes.add(like); - } - - // 좋아요 제거 메서드 - public void removeLike(Like like) { - likes.remove(like); - } - - public void update(UpdateProductRequest modifiedProduct) { - this.name = modifiedProduct.getProductName(); - this.description = modifiedProduct.getDescription(); - this.category = modifiedProduct.getCategory(); - this.minPrice = modifiedProduct.getMinPrice(); - } - - public boolean isOwner(Long userId) { - return this.user.getId().equals(userId); - } - - public void addImages(List images) { - images.forEach(this::addImage); - } - - public void removeImages(List images) { - images.forEach(this::removeImage); - } - - private void addImage(Image image) { - images.add(image); - image.specifyProduct(this); - } - - private void removeImage(Image image) { - images.remove(image); - image.specifyProduct(null); - } - - public String getFirstImageCdnPath() { - return images.stream() - .filter(image -> image.getSequence() == 1) - .map(Image::getCdnPath) // cdnPath 속성만 추출 - .findFirst() - .orElseThrow(() -> new ProductException(ProductErrorCode.IMAGE_NOT_FOUND)); - } - - public List getLikeUserIds() { - return likes.stream() - .map(like -> like.getUser().getId()) - .distinct() - .toList(); - } - - public void validateImageSize() { - long count = this.images.size(); - if (count < 1) { - throw new ImageException(NO_IMAGES_PROVIDED); - } else if (count > 5) { - throw new ImageException(MAX_IMAGE_COUNT_EXCEEDED); - } - } - -} diff --git a/src/main/java/org/chzz/market/domain/product/error/ProductErrorCode.java b/src/main/java/org/chzz/market/domain/product/error/ProductErrorCode.java deleted file mode 100644 index b7c173c2..00000000 --- a/src/main/java/org/chzz/market/domain/product/error/ProductErrorCode.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.chzz.market.domain.product.error; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.chzz.market.common.error.ErrorCode; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum ProductErrorCode implements ErrorCode { - PRODUCT_REGISTER_FAILED(HttpStatus.BAD_REQUEST, "상품 등록에 실패했습니다."), - INVALID_PRODUCT_STATE(HttpStatus.BAD_REQUEST, "상품 상태가 유효하지 않습니다."), - ALREADY_IN_AUCTION(HttpStatus.BAD_REQUEST, "이미 정식경매로 등록된 상품입니다."), - PRODUCT_ALREADY_AUCTIONED(HttpStatus.BAD_REQUEST, "상품이 이미 경매로 등록되어 삭제할 수 없습니다."), - FORBIDDEN_PRODUCT_ACCESS(HttpStatus.FORBIDDEN, "상품에 접근할 수 없습니다."), - PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), - IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "상품 이미지를 찾을 수 없습니다."), - PRODUCT_NOT_FOUND_OR_IN_AUCTION(HttpStatus.NOT_FOUND, "상품을 찾을 수 없거나 경매 상태입니다."); - - private final HttpStatus httpStatus; - private final String message; - - public static class Const { - public static final String PRODUCT_REGISTER_FAILED = "PRODUCT_REGISTER_FAILED"; - public static final String INVALID_PRODUCT_STATE = "INVALID_PRODUCT_STATE"; - public static final String ALREADY_IN_AUCTION = "ALREADY_IN_AUCTION"; - public static final String PRODUCT_ALREADY_AUCTIONED = "PRODUCT_ALREADY_AUCTIONED"; - public static final String FORBIDDEN_PRODUCT_ACCESS = "FORBIDDEN_PRODUCT_ACCESS"; - public static final String PRODUCT_NOT_FOUND = "PRODUCT_NOT_FOUND"; - public static final String IMAGE_NOT_FOUND = "IMAGE_NOT_FOUND"; - public static final String PRODUCT_NOT_FOUND_OR_IN_AUCTION = "PRODUCT_NOT_FOUND_OR_IN_AUCTION"; - } -} diff --git a/src/main/java/org/chzz/market/domain/product/error/ProductException.java b/src/main/java/org/chzz/market/domain/product/error/ProductException.java deleted file mode 100644 index 0e658994..00000000 --- a/src/main/java/org/chzz/market/domain/product/error/ProductException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.chzz.market.domain.product.error; - -import org.chzz.market.common.error.ErrorCode; -import org.chzz.market.common.error.exception.BusinessException; - -public class ProductException extends BusinessException { - public ProductException(final ErrorCode errorCode) { - super(errorCode); - } -} diff --git a/src/main/java/org/chzz/market/domain/product/repository/ProductRepository.java b/src/main/java/org/chzz/market/domain/product/repository/ProductRepository.java deleted file mode 100644 index e4334d5d..00000000 --- a/src/main/java/org/chzz/market/domain/product/repository/ProductRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.chzz.market.domain.product.repository; - -import io.lettuce.core.dynamic.annotation.Param; -import java.util.Optional; -import org.chzz.market.domain.product.entity.Product; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -public interface ProductRepository extends JpaRepository, ProductRepositoryCustom { - - /* - * 상품 아이디로 경매가 존재하는지 확인 - */ - @Query("SELECT p FROM Product p " + - "LEFT JOIN Auction a ON a.product = p " + - "WHERE p.id = :id AND a.id IS NULL") - Optional findPreOrder(@Param("id") Long id); - - /* - * 사용자가 등록한 사전 등록 상품 수 조회 - */ - @Query("SELECT COUNT(p) FROM Product p " + - "LEFT JOIN Auction a ON p.id = a.product.id " + - "WHERE p.user.id = :userId AND a IS NULL") - long countPreRegisteredProductsByUserId(@Param("userId") Long userId); -} diff --git a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustom.java b/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustom.java deleted file mode 100644 index 71211054..00000000 --- a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustom.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.chzz.market.domain.product.repository; - -import static org.chzz.market.domain.product.entity.Product.Category; - -import java.util.Optional; -import org.chzz.market.domain.product.dto.ProductDetailsResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.entity.Product; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface ProductRepositoryCustom { - /** - * 카테고리와 정렬 조건에 따라 사전 등록 상품 리스트를 조회합니다. - * @param category 카테고리 - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 사전 등록 상품 리스트 - */ - Page findProductsByCategory(Category category, Long userId, Pageable pageable); - - /** - * 사용자 ID와 상품 ID에 따라 사전 등록 상품 상세 정보를 조회합니다. - * @param productId 상품 ID - * @param userId 사용자 ID - * @return 상품 상세 정보 - */ - Optional findProductDetailsById(Long productId, Long userId); - - /** - * 사용자 닉네임에 따라 사용자가 등록한 사전 등록 상품 리스트를 조회합니다. - * @param nickname 사용자 닉네임 - * @param pageable 페이징 정보 - * @return 페이징된 사전 등록 상품 리스트 - */ - Page findProductsByNickname(String nickname, Pageable pageable); - - /** - * 사용자 인증정보를 통해 사용자가 등록한 상품 리스트 조회 - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return - */ - Page findProductsByUserId(Long userId, Pageable pageable); - /** - * 사용자 ID에 따라 사용자가 참여한 사전 경매 리스트를 조회합니다. - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 사전 경매 리스트 - */ - Page findLikedProductsByUserId(Long userId, Pageable pageable); - - /** - * 이미지를 fetch한 상품 조회 - * @param productId 조회할 상품 ID - * @return - */ - Optional findProductByIdWithImage(Long productId); -} diff --git a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImpl.java b/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImpl.java deleted file mode 100644 index dc35dad9..00000000 --- a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImpl.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.chzz.market.domain.product.repository; - -import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilder; -import static org.chzz.market.common.util.QuerydslUtil.nullSafeBuilderIgnore; -import static org.chzz.market.domain.auction.entity.QAuction.auction; -import static org.chzz.market.domain.image.entity.QImage.image; -import static org.chzz.market.domain.like.entity.QLike.like; -import static org.chzz.market.domain.product.entity.Product.Category; -import static org.chzz.market.domain.product.entity.QProduct.product; -import static org.chzz.market.domain.user.entity.QUser.user; - -import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.impl.JPAQuery; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import java.util.Optional; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.chzz.market.common.util.QuerydslOrder; -import org.chzz.market.common.util.QuerydslOrderProvider; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.image.dto.QImageResponse; -import org.chzz.market.domain.product.dto.ProductDetailsResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.dto.QProductDetailsResponse; -import org.chzz.market.domain.product.dto.QProductResponse; -import org.chzz.market.domain.product.entity.Product; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.support.PageableExecutionUtils; - -@RequiredArgsConstructor -public class ProductRepositoryCustomImpl implements ProductRepositoryCustom { - private final JPAQueryFactory jpaQueryFactory; - private final QuerydslOrderProvider querydslOrderProvider; - - /** - * 사전 등록 상품 리스트를 조회합니다. - * - * @param category 카테고리 - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 사전 등록 상품 리스트 - */ - @Override - public Page findProductsByCategory(Category category, Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(product) - .leftJoin(auction).on(auction.product.id.eq(product.id)) - .where(auction.id.isNull().and(categoryEqIgnoreNull(category))); - - List content = baseQuery - .select(new QProductResponse( - product.id, - product.name, - image.cdnPath, - product.minPrice, - product.likes.size().longValue(), - isProductLikedByUser(userId), - userIdEq(userId) - )) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .join(product.user, user) - .groupBy(product.id, product.name, image.cdnPath, product.minPrice) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery.select(product.countDistinct()); - - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); - } - - /** - * 사용자 ID와 상품 ID에 따라 사전 등록 상품 상세 정보를 조회합니다. - * - * @param productId 상품 ID - * @param userId 사용자 ID - * @return 상품 상세 정보 - */ - @Override - public Optional findProductDetailsById(Long productId, Long userId) { - - Optional result = Optional.ofNullable(jpaQueryFactory - .select(new QProductDetailsResponse( - product.id, - product.name, - user.nickname, - user.profileImageUrl, - product.minPrice, - product.updatedAt, - product.description, - product.likes.size().longValue(), - isProductLikedByUser(userId), - userIdEq(userId), - product.category - )) - .from(product) - .leftJoin(auction).on(auction.product.id.eq(product.id)) - .join(product.user, user) - .where(auction.id.isNull().and(product.id.eq(productId))) - .fetchOne()); - - // 이미지 목록 추가 - result.ifPresent(response -> response.addImageList(getImagesByProductId(productId))); - return result; - } - - /** - * 사용자 닉네임에 따라 사용자가 등록한 사전 등록 상품 리스트를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @param pageable 페이징 정보 - * @return 페이징된 사전 등록 상품 리스트 - */ - @Override - public Page findProductsByNickname(String nickname, Pageable pageable) { - - JPAQuery baseQuery = jpaQueryFactory.from(product) - .join(product.user, user) - .leftJoin(auction).on(auction.product.eq(product)) - .leftJoin(like).on(like.product.eq(product).and(like.user.nickname.eq(nickname))) - .where(auction.isNull().and(user.nickname.eq(nickname))); - - return getProductResponses(pageable, baseQuery); - } - - @Override - public Page findProductsByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory.from(product) - .join(product.user, user) - .leftJoin(auction).on(auction.product.eq(product)) - .leftJoin(like).on(like.product.eq(product).and(like.user.id.eq(userId))) - .where(auction.isNull().and(user.id.eq(userId))); - - return getProductResponses(pageable, baseQuery); - } - - private Page getProductResponses(Pageable pageable, JPAQuery baseQuery) { - List content = baseQuery - .select(new QProductResponse( - product.id, - product.name, - image.cdnPath, - product.minPrice, - product.likes.size().longValue(), - like.isNotNull() - )) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery - .select(product.count()); - - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); - } - - /** - * 사용자 ID에 따라 사용자가 참여한 사전 경매 리스트를 조회합니다. - * - * @param userId 사용자 ID - * @param pageable 페이징 정보 - * @return 페이징된 사전 경매 리스트 - */ - @Override - public Page findLikedProductsByUserId(Long userId, Pageable pageable) { - - JPAQuery baseQuery = jpaQueryFactory.from(product) - .join(product.likes, like) - .join(like.user, user) - .leftJoin(auction).on(auction.product.eq(product)) - .where(user.id.eq(userId).and(auction.isNull())); - - List content = baseQuery - .select(new QProductResponse( - product.id, - product.name, - image.cdnPath, - product.minPrice, - product.likes.size().longValue() - )) - .leftJoin(image).on(image.product.eq(product).and(isRepresentativeImage())) - .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery countQuery = baseQuery - .select(like.count()); - - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); - } - - @Override - public Optional findProductByIdWithImage(Long productId) { - return Optional.ofNullable(jpaQueryFactory.selectFrom(product) - .leftJoin(product.images, image) - .fetchJoin() - .where(product.id.eq(productId)) - .fetchOne()); - } - - /** - * 제품의 이미지 리스트를 조회 - */ - private List getImagesByProductId(Long productId) { - return jpaQueryFactory - .select(new QImageResponse(image.id, image.cdnPath)) - .from(image) - .where(image.product.id.eq(productId)) - .orderBy(image.sequence.asc()) - .fetch(); - } - - /** - * 상품의 대표 이미지를 조회하기 위한 조건을 반환합니다. - * - * @return 대표 이미지(첫 번째 이미지)의 sequence가 1인 조건식 - */ - private BooleanExpression isRepresentativeImage() { - return image.sequence.eq(1); - } - - /** - * 사용자가 특정 상품을 좋아요(Like)했는지 여부를 확인합니다. - * - * @param userId 사용자 ID - * @return 사용자가 해당 상품을 좋아요한 경우 true, 그렇지 않으면 false - */ - private BooleanExpression isProductLikedByUser(Long userId) { - return JPAExpressions.selectOne() - .from(like) - .where(like.product.eq(product) - .and(likeUserIdEq(userId))) - .exists(); - } - - private BooleanBuilder likeUserIdEq(Long userId) { - return nullSafeBuilder(() -> like.user.id.eq(userId)); - } - - private BooleanBuilder categoryEqIgnoreNull(Category category) { - return nullSafeBuilderIgnore(() -> product.category.eq(category)); - } - - private BooleanBuilder userIdEq(Long userId) { - return nullSafeBuilder(() -> user.id.eq(userId)); - } - - @Getter - @AllArgsConstructor(access = AccessLevel.PRIVATE) - public enum ProductOrder implements QuerydslOrder { - POPULARITY("most-liked", product.likes.size().desc()), - NEWEST("product-newest", product.createdAt.desc()); - - private final String name; - private final OrderSpecifier orderSpecifier; - } - -} diff --git a/src/main/java/org/chzz/market/domain/product/service/ProductService.java b/src/main/java/org/chzz/market/domain/product/service/ProductService.java deleted file mode 100644 index abf3093b..00000000 --- a/src/main/java/org/chzz/market/domain/product/service/ProductService.java +++ /dev/null @@ -1,258 +0,0 @@ -package org.chzz.market.domain.product.service; - -import static org.chzz.market.domain.image.error.ImageErrorCode.INVALID_IMAGE_COUNT; -import static org.chzz.market.domain.image.error.ImageErrorCode.MAX_IMAGE_COUNT_EXCEEDED; -import static org.chzz.market.domain.notification.entity.NotificationType.PRE_AUCTION_CANCELED; -import static org.chzz.market.domain.product.error.ProductErrorCode.ALREADY_IN_AUCTION; -import static org.chzz.market.domain.product.error.ProductErrorCode.FORBIDDEN_PRODUCT_ACCESS; -import static org.chzz.market.domain.product.error.ProductErrorCode.PRODUCT_ALREADY_AUCTIONED; -import static org.chzz.market.domain.product.error.ProductErrorCode.PRODUCT_NOT_FOUND; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.notification.event.NotificationEvent; -import org.chzz.market.domain.product.dto.CategoryResponse; -import org.chzz.market.domain.product.dto.DeleteProductResponse; -import org.chzz.market.domain.product.dto.ProductDetailsResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.dto.UpdateProductResponse; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.product.error.ProductException; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.dao.TransientDataAccessException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Retryable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Slf4j -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class ProductService { - private final ImageService imageService; - private final ProductRepository productRepository; - private final AuctionRepository auctionRepository; - private final ApplicationEventPublisher eventPublisher; - - /** - * 사전 등록 상품 목록 조회 - */ - public Page getProductListByCategory(Category category, Long userId, Pageable pageable) { - return productRepository.findProductsByCategory(category, userId, pageable); - } - - /** - * 상품 상세 정보 조회 - */ - public ProductDetailsResponse getProductDetails(Long productId, Long userId) { - if (auctionRepository.existsByProductId(productId)) { - throw new ProductException(ALREADY_IN_AUCTION); - } - return productRepository.findProductDetailsById(productId, userId) - .orElseThrow(() -> new ProductException(PRODUCT_NOT_FOUND)); - } - - /** - * 나의 사전 등록 상품 목록 조회 - */ - public Page getProductListByNickname(String nickname, Pageable pageable) { - return productRepository.findProductsByNickname(nickname, pageable); - } - - public Page getProductListByUserId(Long userId, Pageable pageable) { - return productRepository.findProductsByUserId(userId, pageable); - } - - /** - * 내가 참여한 사전경매 조회 - */ - public Page getLikedProductList(Long userId, Pageable pageable) { - return productRepository.findLikedProductsByUserId(userId, pageable); - } - - /** - * 상품 카테고리 목록 조회 - */ - public List getCategories() { - return Arrays.stream(Category.values()) - .map(category -> new CategoryResponse(category.name(), category.getDisplayName())) - .toList(); - } - - /** - * 사전 등록 상품 수정 - */ - @Transactional - public UpdateProductResponse updateProduct(Long userId, Long productId, UpdateProductRequest request, - Map newImages) { - // 상품 유효성 검사 - Product existingProduct = productRepository.findProductByIdWithImage(productId) - .orElseThrow(() -> new ProductException(PRODUCT_NOT_FOUND)); - - if (!existingProduct.isOwner(userId)) { - throw new ProductException(FORBIDDEN_PRODUCT_ACCESS); - } - - // 경매 등록 상태 유무 유효성 검사 - if (auctionRepository.existsByProductId(productId)) { - throw new ProductException(ALREADY_IN_AUCTION); - } - - // 상품 정보 업데이트 - existingProduct.update(request); - - // 이미지 저장 - updateProductImages(existingProduct, request, newImages); - - log.info("상품 ID {}번에 대한 사전 등록 정보를 업데이트를 완료했습니다.", productId); - return UpdateProductResponse.from(existingProduct); - } - - /** - * 사전 등록 상품 삭제 - */ - @Retryable( - retryFor = {TransientDataAccessException.class}, - maxAttempts = 3, - backoff = @Backoff(delay = 1000) - ) - @Transactional - public DeleteProductResponse deleteProduct(Long productId, Long userId) { - log.info("상품 ID {}번에 해당하는 상품 삭제 프로세스를 시작합니다.", productId); - - // 상품 유효성 검사 - Product product = productRepository.findById(productId) - .orElseThrow(() -> { - log.info("상품 ID {}번에 해당하는 상품을 찾을 수 없습니다.", productId); - return new ProductException(PRODUCT_NOT_FOUND); - }); - - if (!product.isOwner(userId)) { - throw new ProductException(FORBIDDEN_PRODUCT_ACCESS); - } - - // 경매 등록 여부 확인 - if (auctionRepository.existsByProductId(productId)) { - log.info("상품 ID {}번은 이미 경매로 등록되어 삭제할 수 없습니다.", productId); - throw new ProductException(PRODUCT_ALREADY_AUCTIONED); - } - - deleteProductImages(product); - productRepository.delete(product); - - // 좋아요 누른 사용자 ID 추출 - List likedUserIds = product.getLikes().stream() - .map(like -> like.getUser().getId()) - .distinct() - .toList(); - if (!likedUserIds.isEmpty()) { - eventPublisher.publishEvent( - NotificationEvent.createSimpleNotification(likedUserIds, PRE_AUCTION_CANCELED, - PRE_AUCTION_CANCELED.getMessage(product.getName()), - null)); // TODO: 사전 등록 취소 (soft delete 로 변경시 이미지 추가) - } - - log.info("사전 등록 상품 ID{}번에 해당하는 상품을 성공적으로 삭제하였습니다. (좋아요 누른 사용자 수: {})", productId, likedUserIds.size()); - - return DeleteProductResponse.ofPreRegistered(product, likedUserIds.size()); - } - - /** - * 상품 이미지 업데이트 - */ - private void updateProductImages(Product product, - UpdateProductRequest request, - Map images) { - Map newImages = removeRequestKey(images); - Map imageSequence = Optional.ofNullable(request.getImageSequence()) - .orElse(Collections.emptyMap()); - - // 요청에 대한 총 이미지 수 검증 - validateTotalImageCount(imageSequence.size() + newImages.size()); - - // 기존 이미지 처리 (업데이트할 이미지와 삭제할 이미지 구분) - processExistingImages(product, imageSequence); - - // 새 이미지가 있는 경우 - if (!newImages.isEmpty()) { - uploadAndAddNewImages(product, newImages); - } - product.validateImageSize(); - } - - /** - * 상품 이미지 삭제 - */ - private void deleteProductImages(Product product) { - List imageUrls = product.getImages().stream() - .map(Image::getCdnPath) - .toList(); - - imageService.deleteUploadImages(imageUrls); - log.info("상품 ID {}번에 해당하는 상품의 이미지를 모두 삭제하였습니다.", product.getId()); - } - - /** - * 새로운 이미지 맵에서 "request" 키를 제거하고, null 값을 처리 (@RequestPart 사용으로 인한) - */ - private Map removeRequestKey(Map newImages) { - if (newImages != null) { - newImages.remove("request"); - } - return newImages != null ? newImages : Collections.emptyMap(); - } - - /** - * 총 이미지 수(기존 이미지 갯수 + 새롭게 업로드할 이미지 갯수) 가 유효한지 검증합니다. - */ - private void validateTotalImageCount(int totalSize) { - if (totalSize > 5) { - throw new ProductException(MAX_IMAGE_COUNT_EXCEEDED); - } else if (totalSize == 0) { - throw new ProductException(INVALID_IMAGE_COUNT); - } - } - - /** - * 기존 이미지 업데이트 및 삭제 처리 - */ - private void processExistingImages(Product product, Map imageSequence) { - List imagesToUpdate = new ArrayList<>(); - List imagesToRemove = new ArrayList<>(); - - product.getImages().forEach(image -> { - if (imageSequence.containsKey(image.getId())) { - imagesToUpdate.add(image); // 업데이트할 이미지 - } else { - imagesToRemove.add(image); // 삭제할 이미지 - } - }); - product.removeImages(imagesToRemove); // 삭제할 이미지 처리 - imageService.updateImageSequences(imagesToUpdate, imageSequence); // 시퀀스 업데이트할 이미지 처리 - } - - /** - * 상품 수정 시 새로운 이미지 업로드 - */ - private void uploadAndAddNewImages(Product product, Map newImages) { - List newImageEntities = imageService.uploadSequentialImages(product, newImages); - product.addImages(newImageEntities); - log.info("상품 ID {}번의 새 이미지를 성공적으로 저장하였습니다.", product.getId()); - } -} diff --git a/src/main/java/org/chzz/market/domain/user/controller/UserApi.java b/src/main/java/org/chzz/market/domain/user/controller/UserApi.java index 5f16137b..dc7ccc2e 100644 --- a/src/main/java/org/chzz/market/domain/user/controller/UserApi.java +++ b/src/main/java/org/chzz/market/domain/user/controller/UserApi.java @@ -23,9 +23,6 @@ public interface UserApi { @Operation(summary = "나의 프로필 조회") ResponseEntity getUserProfileById(Long userId); - @Operation(summary = "사용자 프로필 조회 (닉네임 기반) - 현재 사용 X") - ResponseEntity getUserProfileByNickname(String nickname); - @Operation(summary = "닉네임 중복 확인") ResponseEntity checkNickname(@Length(min = 1, max = 15) String nickname); diff --git a/src/main/java/org/chzz/market/domain/user/controller/UserController.java b/src/main/java/org/chzz/market/domain/user/controller/UserController.java index b34ead4e..f275bac5 100644 --- a/src/main/java/org/chzz/market/domain/user/controller/UserController.java +++ b/src/main/java/org/chzz/market/domain/user/controller/UserController.java @@ -3,7 +3,6 @@ import static org.chzz.market.common.filter.JWTFilter.AUTHORIZATION_HEADER; import static org.chzz.market.common.filter.JWTFilter.BEARER_TOKEN_PREFIX; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -59,21 +58,13 @@ public ResponseEntity getUserProfileById(@LoginUser Long us return ResponseEntity.ok(userService.getUserProfileById(userId)); } - /** - * 사용자 프로필 조회 (닉네임 기반) 현재 사용 X - */ - @Override - @GetMapping("/{nickname}") - public ResponseEntity getUserProfileByNickname(@PathVariable String nickname) { - return ResponseEntity.ok(userService.getUserProfileByNickname(nickname)); - } - /** * 닉네임 중복 확인 */ @Override @GetMapping("/check/nickname/{nickname}") - public ResponseEntity checkNickname(@PathVariable @Length(min = 1, max = 15) String nickname) { + public ResponseEntity checkNickname( + @PathVariable @Length(min = 1, max = 15) String nickname) { return ResponseEntity.ok((userService.checkNickname(nickname))); } diff --git a/src/main/java/org/chzz/market/domain/user/dto/response/UserProfileResponse.java b/src/main/java/org/chzz/market/domain/user/dto/response/UserProfileResponse.java index 4c4b4708..bafdcbe3 100644 --- a/src/main/java/org/chzz/market/domain/user/dto/response/UserProfileResponse.java +++ b/src/main/java/org/chzz/market/domain/user/dto/response/UserProfileResponse.java @@ -1,13 +1,11 @@ package org.chzz.market.domain.user.dto.response; -import com.fasterxml.jackson.annotation.JsonInclude; import org.chzz.market.domain.user.entity.User; public record UserProfileResponse( String nickname, String bio, String profileImageUrl, - @JsonInclude(JsonInclude.Include.NON_NULL) String providerType, ParticipationCountsResponse participantCount, Long preRegisterCount, @@ -16,13 +14,12 @@ public record UserProfileResponse( public static UserProfileResponse of(User user, ParticipationCountsResponse counts, Long preRegisterCount, - Long registeredAuctionCount, - boolean includeProviderType) { + Long registeredAuctionCount) { return new UserProfileResponse( user.getNickname(), user.getBio(), user.getProfileImageUrl(), - includeProviderType ? user.getProviderType().name() : null, + user.getProviderType().name(), counts, preRegisterCount, registeredAuctionCount diff --git a/src/main/java/org/chzz/market/domain/user/service/UserService.java b/src/main/java/org/chzz/market/domain/user/service/UserService.java index b8bfbce4..89929b99 100644 --- a/src/main/java/org/chzz/market/domain/user/service/UserService.java +++ b/src/main/java/org/chzz/market/domain/user/service/UserService.java @@ -5,9 +5,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.product.repository.ProductRepository; import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest; import org.chzz.market.domain.user.dto.request.UserCreateRequest; import org.chzz.market.domain.user.dto.response.NicknameAvailabilityResponse; @@ -28,20 +29,19 @@ public class UserService { private final ImageService imageService; private final UserRepository userRepository; private final AuctionRepository auctionRepository; - private final ProductRepository productRepository; + private final AuctionQueryRepository auctionQueryRepository; /** * 사용자 프로필 조회 (유저 ID 기반) */ public UserProfileResponse getUserProfileById(Long userId) { - return getUserProfileInternal(findUserById(userId), true); - } - - /** - * 사용자 프로필 조회 (닉네임 기반) - */ - public UserProfileResponse getUserProfileByNickname(String nickname) { - return getUserProfileInternal(findUserByNickname(nickname), false); + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + long preAuctionCount = auctionRepository.countBySellerIdAndStatusIn(userId, AuctionStatus.PRE); + long officialAuctionCount = auctionRepository.countBySellerIdAndStatusIn(userId, AuctionStatus.PROCEEDING, + AuctionStatus.ENDED); + ParticipationCountsResponse counts = auctionQueryRepository.getParticipationCounts(userId); + return UserProfileResponse.of(user, counts, preAuctionCount, officialAuctionCount); } /** @@ -94,34 +94,6 @@ public void updateUserProfile(Long userId, MultipartFile file, UpdateUserProfile existingUser.updateProfile(request, profileImageUrl); } - /** - * 내 프로필 조회 - */ - private UserProfileResponse getUserProfileInternal(User user, boolean includeProviderType) { - long preRegisterCount = productRepository.countPreRegisteredProductsByUserId(user.getId()); - long registeredAuctionCount = auctionRepository.countByProductUserId(user.getId()); - - ParticipationCountsResponse counts = auctionRepository.getParticipationCounts(user.getId()); - - return UserProfileResponse.of(user, counts, preRegisterCount, registeredAuctionCount, includeProviderType); - } - - /** - * 닉네임으로 사용자 조회 - */ - private User findUserByNickname(String nickname) { - return userRepository.findByNickname(nickname) - .orElseThrow(() -> new UserException(USER_NOT_FOUND)); - } - - /** - * ID로 사용자 조회 - */ - private User findUserById(Long userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new UserException(USER_NOT_FOUND)); - } - /** * 회원 프로필 이미지 변경 */ diff --git a/src/main/resources/db/migration/V10__modify_field_name.sql b/src/main/resources/db/migration/V10__modify_field_name.sql deleted file mode 100644 index 677bf0df..00000000 --- a/src/main/resources/db/migration/V10__modify_field_name.sql +++ /dev/null @@ -1,14 +0,0 @@ --- 파일명: V10__modify_field_name.sql --- 파일 설명: 일부 테이블 필드 이름 변경 --- 작성일: 2024-11-5 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -ALTER TABLE `payment` - CHANGE COLUMN `order_id` `order_no` VARCHAR(255) NOT NULL ; - --- 테이블 이름 변경 -ALTER TABLE address RENAME TO delivery; - --- 컬럼 이름 변경 -ALTER TABLE delivery CHANGE address_id delivery_id BIGINT NOT NULL AUTO_INCREMENT; diff --git a/src/main/resources/db/migration/V11__notification_image_path.sql b/src/main/resources/db/migration/V11__notification_image_path.sql deleted file mode 100644 index 2d5f9dbc..00000000 --- a/src/main/resources/db/migration/V11__notification_image_path.sql +++ /dev/null @@ -1,21 +0,0 @@ --- 파일명: V11__notification_image_path.sql --- 파일 설명: notification 테이블에 cdn_path 컬럼을 추가하고 image_id 컬럼을 제거합니다. --- 작성일: 2024-11-5 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - --- 1. cdn_path 컬럼 추가 -ALTER TABLE notification - ADD COLUMN cdn_path VARCHAR(255) DEFAULT NULL AFTER is_deleted; - --- 2. cdn_path 데이터 업데이트 -UPDATE notification n - JOIN image i ON n.image_id = i.image_id -SET n.cdn_path = i.cdn_path; - --- 3. image_id 컬럼 및 외래 키 제약 조건 삭제 -ALTER TABLE notification - DROP FOREIGN KEY FKholipoc9p58ukvigqmd8ejvoo; - -ALTER TABLE notification - DROP COLUMN image_id; diff --git a/src/main/resources/db/migration/V12__modify_bid_field.sql b/src/main/resources/db/migration/V12__modify_bid_field.sql deleted file mode 100644 index 0a22bb74..00000000 --- a/src/main/resources/db/migration/V12__modify_bid_field.sql +++ /dev/null @@ -1,21 +0,0 @@ --- 파일명: V12__modify_bid_field.sql --- 파일 설명: bid 테이블의 필드 수정 --- 작성일: 2024-11-5 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - --- 외래키 제약 조건 삭제 -ALTER TABLE `bid` - DROP FOREIGN KEY `FKhexc6i4j8i0tmpt8bdulp6g3g`, - DROP FOREIGN KEY `FKi1pwg1muxilapowsmifod8jtf`; - --- user_id 컬럼 이름을 bidder_id로 변경 -ALTER TABLE `bid` - CHANGE COLUMN `user_id` `bidder_id` bigint NOT NULL; - --- 인덱스 이름을 auction_id_idx와 bidder_id_idx로 변경 -ALTER TABLE `bid` - DROP INDEX `FKhexc6i4j8i0tmpt8bdulp6g3g`, - DROP INDEX `FKi1pwg1muxilapowsmifod8jtf`, - ADD INDEX `auction_id_idx` (`auction_id`), - ADD INDEX `bidder_id_idx` (`bidder_id`); diff --git a/src/main/resources/db/migration/V13__delete_notification_user_id_foreign_key.sql b/src/main/resources/db/migration/V13__delete_notification_user_id_foreign_key.sql deleted file mode 100644 index 6b555c78..00000000 --- a/src/main/resources/db/migration/V13__delete_notification_user_id_foreign_key.sql +++ /dev/null @@ -1,13 +0,0 @@ --- 파일명: V13__delete_notification_user_id_foreign_key.sql --- 파일 설명: notification 테이블의 외래키 제거 및 인덱스 이름 변경 --- 작성일: 2024-11-5 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - --- user_id 외래키 제약 조건 삭제 -ALTER TABLE notification - DROP FOREIGN KEY FKnk4ftb5am9ubmkv1661h15ds9; - --- user_id 인덱스 이름을 user_id_idx로 변경 -ALTER TABLE notification - RENAME INDEX FKnk4ftb5am9ubmkv1661h15ds9 TO user_id_idx; diff --git a/src/main/resources/db/migration/V14__create_v2_auction.sql b/src/main/resources/db/migration/V14__create_v2_auction.sql deleted file mode 100644 index 0f8628b8..00000000 --- a/src/main/resources/db/migration/V14__create_v2_auction.sql +++ /dev/null @@ -1,26 +0,0 @@ --- 파일명: V14__create_v2_auction.sql --- 파일 설명: v2 auction 테이블 생성 --- 작성일: 2024-11-8 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -CREATE TABLE auction_v2 -( - auction_id BIGINT AUTO_INCREMENT NOT NULL, - seller_id BIGINT NULL, - name VARCHAR(255) NOT NULL, - `description` VARCHAR(1000) NULL, - min_price INT NULL, - category VARCHAR(30) NOT NULL, - status VARCHAR(20) NULL, - end_date_time datetime NULL, - winner_id BIGINT NULL, - like_count INT NULL, - bid_count INT NULL, - created_at datetime NULL, - updated_at datetime NULL, - CONSTRAINT pk_auction_v2 PRIMARY KEY (auction_id) -); - -ALTER TABLE auction_v2 - ADD CONSTRAINT FK_AUCTION_V2_ON_SELLER FOREIGN KEY (seller_id) REFERENCES users (user_id); diff --git a/src/main/resources/db/migration/V15__create_like_image.sql b/src/main/resources/db/migration/V15__create_like_image.sql deleted file mode 100644 index c3325614..00000000 --- a/src/main/resources/db/migration/V15__create_like_image.sql +++ /dev/null @@ -1,32 +0,0 @@ --- 파일명: V15__create_like_image.sql --- 파일 설명: v2 like, image --- 작성일: 2024-11-12 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -CREATE TABLE imagev2 -( - image_id BIGINT AUTO_INCREMENT NOT NULL, - auction_id BIGINT NULL, - cdn_path VARCHAR(255) NOT NULL, - sequence INT NULL, - created_at datetime NULL, - updated_at datetime NULL, - CONSTRAINT pk_imagev2 PRIMARY KEY (image_id) -); - -CREATE TABLE likes -( - like_id BIGINT AUTO_INCREMENT NOT NULL, - user_id BIGINT NOT NULL, - auction_id BIGINT NOT NULL, - created_at datetime NULL, - updated_at datetime NULL, - CONSTRAINT pk_likes PRIMARY KEY (like_id), - CONSTRAINT uc_likes_user_auction UNIQUE (user_id, auction_id) -); - -CREATE INDEX idx_15d5b1726f3551b467ed898fd ON imagev2 (auction_id, image_id, cdn_path); - -ALTER TABLE imagev2 - ADD CONSTRAINT FK_IMAGEV2_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction_v2 (auction_id); diff --git a/src/main/resources/db/migration/V16__create_v2_order_payment.sql b/src/main/resources/db/migration/V16__create_v2_order_payment.sql deleted file mode 100644 index cc9ae76e..00000000 --- a/src/main/resources/db/migration/V16__create_v2_order_payment.sql +++ /dev/null @@ -1,55 +0,0 @@ --- 파일명: V16__create_v2_order_payment.sql --- 파일 설명: v2 order, payment, 경매 API 전환완료시 삭제 예정 --- 작성일: 2024-11-18 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -CREATE TABLE orders_v2 -( - order_id BIGINT AUTO_INCREMENT NOT NULL, - order_no VARCHAR(255) NOT NULL, - buyer_id BIGINT NOT NULL, - payment_id BIGINT NOT NULL, - amount BIGINT NOT NULL, - delivery_memo VARCHAR(255) NULL, - road_address VARCHAR(255) NOT NULL, - jibun VARCHAR(255) NOT NULL, - zipcode VARCHAR(255) NOT NULL, - detail_address VARCHAR(255) NOT NULL, - recipient_name VARCHAR(255) NOT NULL, - phone_number VARCHAR(255) NOT NULL, - method VARCHAR(30) NOT NULL, - auction_id BIGINT NULL, - CONSTRAINT pk_orders_v2 PRIMARY KEY (order_id) -); - -CREATE TABLE paymentv2 -( - payment_id BIGINT AUTO_INCREMENT NOT NULL, - created_at datetime NULL, - updated_at datetime NULL, - user_id BIGINT NOT NULL, - auction_id BIGINT NOT NULL, - amount BIGINT NOT NULL, - method VARCHAR(30) NOT NULL, - status VARCHAR(30) NOT NULL, - order_no VARCHAR(255) NOT NULL, - payment_key VARCHAR(255) NOT NULL, - CONSTRAINT pk_paymentv2 PRIMARY KEY (payment_id) -); - -ALTER TABLE paymentv2 - ADD CONSTRAINT uc_paymentv2_orderno UNIQUE (order_no); - -ALTER TABLE orders_v2 - ADD CONSTRAINT FK_ORDERS_V2_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction_v2 (auction_id); - -ALTER TABLE paymentv2 - ADD CONSTRAINT FK_PAYMENTV2_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction_v2 (auction_id); - -ALTER TABLE paymentv2 - ADD CONSTRAINT FK_PAYMENTV2_ON_USER FOREIGN KEY (user_id) REFERENCES users (user_id); - -ALTER TABLE auction_v2 - MODIFY COLUMN bid_count BIGINT NULL, - MODIFY COLUMN like_count BIGINT NULL; diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 4e257655..4cf0e40e 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -1,172 +1,165 @@ -DROP TABLE IF EXISTS `users`; -CREATE TABLE `users` +CREATE TABLE auction ( - `user_id` bigint NOT NULL AUTO_INCREMENT, - `nickname` varchar(25) DEFAULT NULL, - `email` varchar(255) NOT NULL, - `bio` text, - `link` varchar(255) DEFAULT NULL, - `provider_id` varchar(255) NOT NULL, - `provider_type` varchar(20) DEFAULT NULL, - `customer_key` binary(16) NOT NULL, - `user_role` varchar(20) DEFAULT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`user_id`), - UNIQUE KEY `UK_tjpwcsm4fvnedy6uimbl9g8mm` (`customer_key`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + auction_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + seller_id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + `description` VARCHAR(1000) NULL, + min_price INT NULL, + category VARCHAR(30) NOT NULL, + end_date_time datetime NULL, + status VARCHAR(20) NULL, + winner_id BIGINT NULL, + like_count BIGINT NULL, + bid_count BIGINT NULL, + CONSTRAINT pk_auction PRIMARY KEY (auction_id) +); -DROP TABLE IF EXISTS `product`; -CREATE TABLE `product` +CREATE TABLE bid ( - `product_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `name` varchar(255) NOT NULL, - `description` varchar(1000) DEFAULT NULL, - `category` varchar(30) NOT NULL, - `min_price` int DEFAULT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`product_id`), - KEY `idx_product_id_name` (`product_id`,`name`), - KEY `FK47nyv78b35eaufr6aa96vep6n` (`user_id`), - CONSTRAINT `FK47nyv78b35eaufr6aa96vep6n` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + bid_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + bidder_id BIGINT NOT NULL, + auction_id BIGINT NOT NULL, + amount BIGINT NOT NULL, + count INT DEFAULT 2 NOT NULL, + status VARCHAR(255) NULL, + CONSTRAINT pk_bid PRIMARY KEY (bid_id) +); -DROP TABLE IF EXISTS `auction`; -CREATE TABLE `auction` +CREATE TABLE delivery ( - `auction_id` bigint NOT NULL AUTO_INCREMENT, - `product_id` bigint DEFAULT NULL, - `status` varchar(20) DEFAULT NULL, - `end_date_time` datetime(6) DEFAULT NULL, - `winner_id` bigint DEFAULT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`auction_id`), - UNIQUE KEY `UK_kofsgcp79eu3d1puixs92584u` (`product_id`), - KEY `idx_auction_end_date_time` (`end_date_time`), - CONSTRAINT `FKik2rw5as7p6r3y92mlu2hbrrj` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + delivery_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + user_id BIGINT NOT NULL, + road_address VARCHAR(255) NOT NULL, + jibun VARCHAR(255) NOT NULL, + zipcode VARCHAR(255) NOT NULL, + detail_address VARCHAR(255) NOT NULL, + recipient_name VARCHAR(255) NOT NULL, + phone_number VARCHAR(255) NOT NULL, + is_default BIT(1) NOT NULL, + CONSTRAINT pk_delivery PRIMARY KEY (delivery_id) +); -DROP TABLE IF EXISTS `image`; -CREATE TABLE `image` +CREATE TABLE image ( - `image_id` bigint NOT NULL AUTO_INCREMENT, - `product_id` bigint DEFAULT NULL, - `cdn_path` varchar(255) NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`image_id`), - KEY `IDXn5mhtpce0785mrnv50axnhlj2` (`product_id`,`image_id`,`cdn_path`), - CONSTRAINT `FKgpextbyee3uk9u6o2381m7ft1` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + image_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + auction_id BIGINT NULL, + cdn_path VARCHAR(255) NOT NULL, + sequence INT NULL, + CONSTRAINT pk_image PRIMARY KEY (image_id) +); -DROP TABLE IF EXISTS `notification`; -CREATE TABLE `notification` +CREATE TABLE likes ( - `notification_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `image_id` bigint DEFAULT NULL, - `auction_id` bigint DEFAULT NULL, - `type` varchar(31) NOT NULL, - `message` varchar(255) NOT NULL, - `is_read` bit(1) NOT NULL, - `is_deleted` bit(1) NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`notification_id`), - KEY `FKholipoc9p58ukvigqmd8ejvoo` (`image_id`), - KEY `FKnk4ftb5am9ubmkv1661h15ds9` (`user_id`), - CONSTRAINT `FKholipoc9p58ukvigqmd8ejvoo` FOREIGN KEY (`image_id`) REFERENCES `image` (`image_id`), - CONSTRAINT `FKnk4ftb5am9ubmkv1661h15ds9` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + like_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + user_id BIGINT NOT NULL, + auction_id BIGINT NOT NULL, + CONSTRAINT pk_likes PRIMARY KEY (like_id) +); -DROP TABLE IF EXISTS `address`; -CREATE TABLE `address` +CREATE TABLE notification ( - `address_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `zipcode` varchar(255) DEFAULT NULL, - `road_address` varchar(255) DEFAULT NULL, - `jibun` varchar(255) DEFAULT NULL, - `detail_address` varchar(255) DEFAULT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`address_id`), - KEY `FK6i66ijb8twgcqtetl8eeeed6v` (`user_id`), - CONSTRAINT `FK6i66ijb8twgcqtetl8eeeed6v` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + notification_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + user_id BIGINT NOT NULL, + cdn_path VARCHAR(255) NULL, + message VARCHAR(255) NOT NULL, + is_read BIT(1) NOT NULL, + is_deleted BIT(1) NOT NULL, + type VARCHAR(255) NULL, + auction_id BIGINT NULL, + CONSTRAINT pk_notification PRIMARY KEY (notification_id) +); -DROP TABLE IF EXISTS `bank_account`; -CREATE TABLE `bank_account` +CREATE TABLE orders ( - `bank_account_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `name` varchar(255) NOT NULL, - `number` varchar(255) NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`bank_account_id`), - KEY `FKftsfxon3d4ectm5bv7glrhlko` (`user_id`), - CONSTRAINT `FKftsfxon3d4ectm5bv7glrhlko` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + order_id BIGINT AUTO_INCREMENT NOT NULL, + order_no VARCHAR(255) NOT NULL, + buyer_id BIGINT NOT NULL, + payment_id BIGINT NOT NULL, + amount BIGINT NOT NULL, + delivery_memo VARCHAR(255) NULL, + road_address VARCHAR(255) NOT NULL, + jibun VARCHAR(255) NOT NULL, + zipcode VARCHAR(255) NOT NULL, + detail_address VARCHAR(255) NOT NULL, + recipient_name VARCHAR(255) NOT NULL, + phone_number VARCHAR(255) NOT NULL, + method VARCHAR(30) NOT NULL, + auction_id BIGINT NULL, + CONSTRAINT pk_orders PRIMARY KEY (order_id) +); -DROP TABLE IF EXISTS `bid`; -CREATE TABLE `bid` +CREATE TABLE payment ( - `bid_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `auction_id` bigint NOT NULL, - `amount` bigint NOT NULL, - `count` int NOT NULL DEFAULT '3', - `status` varchar(255) DEFAULT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`bid_id`), - KEY `FKhexc6i4j8i0tmpt8bdulp6g3g` (`auction_id`), - KEY `FKi1pwg1muxilapowsmifod8jtf` (`user_id`), - CONSTRAINT `FKhexc6i4j8i0tmpt8bdulp6g3g` FOREIGN KEY (`auction_id`) REFERENCES `auction` (`auction_id`), - CONSTRAINT `FKi1pwg1muxilapowsmifod8jtf` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + payment_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + user_id BIGINT NOT NULL, + auction_id BIGINT NOT NULL, + amount BIGINT NOT NULL, + method VARCHAR(30) NOT NULL, + status VARCHAR(30) NOT NULL, + order_no VARCHAR(255) NOT NULL, + payment_key VARCHAR(255) NOT NULL, + CONSTRAINT pk_payment PRIMARY KEY (payment_id) +); -DROP TABLE IF EXISTS `like_table`; -CREATE TABLE `like_table` +CREATE TABLE users ( - `like_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `product_id` bigint NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`like_id`), - UNIQUE KEY `UKspmgcymhkuyqi5k8jb3k597kn` (`user_id`,`product_id`), - KEY `FK5q2gfd8rptdrkftmoqje3jjbw` (`product_id`), - CONSTRAINT `FK1iv11yge276b5tut7r0151m98` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`), - CONSTRAINT `FK5q2gfd8rptdrkftmoqje3jjbw` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + user_id BIGINT AUTO_INCREMENT NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, + provider_id VARCHAR(255) NOT NULL, + nickname VARCHAR(25) NULL, + email VARCHAR(255) NOT NULL, + bio TEXT NULL, + profile_image_url VARCHAR(255) NULL, + user_role VARCHAR(20) NULL, + provider_type VARCHAR(20) NULL, + customer_key BINARY(16) NOT NULL, + CONSTRAINT pk_users PRIMARY KEY (user_id) +); -DROP TABLE IF EXISTS `payment`; -CREATE TABLE `payment` -( - `payment_id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL, - `auction_id` bigint NOT NULL, - `order_id` varchar(255) NOT NULL, - `amount` bigint NOT NULL, - `method` varchar(30) NOT NULL, - `status` varchar(30) NOT NULL, - `payment_key` varchar(255) NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - PRIMARY KEY (`payment_id`), - UNIQUE KEY `UK_mf7n8wo2rwrxsd6f3t9ub2mep` (`order_id`), - KEY `FKb0ekvs48lsday0ohucw8a1yi` (`auction_id`), - KEY `FKmi2669nkjesvp7cd257fptl6f` (`user_id`), - CONSTRAINT `FKb0ekvs48lsday0ohucw8a1yi` FOREIGN KEY (`auction_id`) REFERENCES `auction` (`auction_id`), - CONSTRAINT `FKmi2669nkjesvp7cd257fptl6f` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +ALTER TABLE likes + ADD CONSTRAINT uc_1f2d6acc739712581f2945dac UNIQUE (user_id, auction_id); + +ALTER TABLE payment + ADD CONSTRAINT uc_payment_orderno UNIQUE (order_no); + +ALTER TABLE users + ADD CONSTRAINT uc_users_customerkey UNIQUE (customer_key); + +CREATE INDEX idx_auction_image ON image (image_id, cdn_path, auction_id); + +ALTER TABLE auction + ADD CONSTRAINT FK_AUCTION_ON_SELLER FOREIGN KEY (seller_id) REFERENCES users (user_id); + +ALTER TABLE delivery + ADD CONSTRAINT FK_DELIVERY_ON_USER FOREIGN KEY (user_id) REFERENCES users (user_id); + +ALTER TABLE image + ADD CONSTRAINT FK_IMAGE_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction (auction_id); + +ALTER TABLE orders + ADD CONSTRAINT FK_ORDERS_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction (auction_id); + +ALTER TABLE payment + ADD CONSTRAINT FK_PAYMENT_ON_AUCTION FOREIGN KEY (auction_id) REFERENCES auction (auction_id); + +ALTER TABLE payment + ADD CONSTRAINT FK_PAYMENT_ON_USER FOREIGN KEY (user_id) REFERENCES users (user_id); -# ------------------------------------------------------------------------------------------------------------------------ DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; CREATE TABLE `QRTZ_JOB_DETAILS` @@ -182,9 +175,11 @@ CREATE TABLE `QRTZ_JOB_DETAILS` `REQUESTS_RECOVERY` varchar(1) NOT NULL, `JOB_DATA` blob, PRIMARY KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`), - KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`), - KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`, `REQUESTS_RECOVERY`), + KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`, `JOB_GROUP`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; CREATE TABLE `QRTZ_TRIGGERS` @@ -206,20 +201,23 @@ CREATE TABLE `QRTZ_TRIGGERS` `MISFIRE_INSTR` smallint DEFAULT NULL, `JOB_DATA` blob, PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), - KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_T_C` (`SCHED_NAME`,`CALENDAR_NAME`), - KEY `IDX_QRTZ_T_G` (`SCHED_NAME`,`TRIGGER_GROUP`), - KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`,`NEXT_FIRE_TIME`), - KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`,`TRIGGER_STATE`,`NEXT_FIRE_TIME`), - KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`), - KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), + KEY `IDX_QRTZ_T_J` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`), + KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`, `JOB_GROUP`), + KEY `IDX_QRTZ_T_C` (`SCHED_NAME`, `CALENDAR_NAME`), + KEY `IDX_QRTZ_T_G` (`SCHED_NAME`, `TRIGGER_GROUP`), + KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`, `TRIGGER_STATE`), + KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `TRIGGER_STATE`), + KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`, `TRIGGER_GROUP`, `TRIGGER_STATE`), + KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`, `NEXT_FIRE_TIME`), + KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`, `TRIGGER_STATE`, `NEXT_FIRE_TIME`), + KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`, `MISFIRE_INSTR`, `NEXT_FIRE_TIME`), + KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`, `MISFIRE_INSTR`, `NEXT_FIRE_TIME`, `TRIGGER_STATE`), + KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`, `MISFIRE_INSTR`, `NEXT_FIRE_TIME`, `TRIGGER_GROUP`, + `TRIGGER_STATE`), CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`; CREATE TABLE `QRTZ_BLOB_TRIGGERS` @@ -229,9 +227,11 @@ CREATE TABLE `QRTZ_BLOB_TRIGGERS` `TRIGGER_GROUP` varchar(190) NOT NULL, `BLOB_DATA` blob, PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), - KEY `SCHED_NAME` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), + KEY `SCHED_NAME` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), CONSTRAINT `QRTZ_BLOB_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_CALENDARS`; CREATE TABLE `QRTZ_CALENDARS` @@ -240,7 +240,9 @@ CREATE TABLE `QRTZ_CALENDARS` `CALENDAR_NAME` varchar(190) NOT NULL, `CALENDAR` blob NOT NULL, PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`; CREATE TABLE `QRTZ_CRON_TRIGGERS` @@ -252,7 +254,9 @@ CREATE TABLE `QRTZ_CRON_TRIGGERS` `TIME_ZONE_ID` varchar(80) DEFAULT NULL, PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), CONSTRAINT `QRTZ_CRON_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`; CREATE TABLE `QRTZ_FIRED_TRIGGERS` @@ -271,13 +275,15 @@ CREATE TABLE `QRTZ_FIRED_TRIGGERS` `IS_NONCONCURRENT` varchar(1) DEFAULT NULL, `REQUESTS_RECOVERY` varchar(1) DEFAULT NULL, PRIMARY KEY (`SCHED_NAME`, `ENTRY_ID`), - KEY `IDX_QRTZ_FT_TRIG_INST_NAME` (`SCHED_NAME`,`INSTANCE_NAME`), - KEY `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY` (`SCHED_NAME`,`INSTANCE_NAME`,`REQUESTS_RECOVERY`), - KEY `IDX_QRTZ_FT_J_G` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_FT_JG` (`SCHED_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_FT_T_G` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), - KEY `IDX_QRTZ_FT_TG` (`SCHED_NAME`,`TRIGGER_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + KEY `IDX_QRTZ_FT_TRIG_INST_NAME` (`SCHED_NAME`, `INSTANCE_NAME`), + KEY `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY` (`SCHED_NAME`, `INSTANCE_NAME`, `REQUESTS_RECOVERY`), + KEY `IDX_QRTZ_FT_J_G` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`), + KEY `IDX_QRTZ_FT_JG` (`SCHED_NAME`, `JOB_GROUP`), + KEY `IDX_QRTZ_FT_T_G` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), + KEY `IDX_QRTZ_FT_TG` (`SCHED_NAME`, `TRIGGER_GROUP`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_LOCKS`; CREATE TABLE `QRTZ_LOCKS` @@ -285,7 +291,9 @@ CREATE TABLE `QRTZ_LOCKS` `SCHED_NAME` varchar(120) NOT NULL, `LOCK_NAME` varchar(40) NOT NULL, PRIMARY KEY (`SCHED_NAME`, `LOCK_NAME`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`; CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` @@ -293,7 +301,9 @@ CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` `SCHED_NAME` varchar(120) NOT NULL, `TRIGGER_GROUP` varchar(190) NOT NULL, PRIMARY KEY (`SCHED_NAME`, `TRIGGER_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`; CREATE TABLE `QRTZ_SCHEDULER_STATE` @@ -303,7 +313,9 @@ CREATE TABLE `QRTZ_SCHEDULER_STATE` `LAST_CHECKIN_TIME` bigint NOT NULL, `CHECKIN_INTERVAL` bigint NOT NULL, PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`; CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` @@ -316,7 +328,9 @@ CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` `TIMES_TRIGGERED` bigint NOT NULL, PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), CONSTRAINT `QRTZ_SIMPLE_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`; CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` @@ -337,4 +351,6 @@ CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` `BOOL_PROP_2` varchar(1) DEFAULT NULL, PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`), CONSTRAINT `QRTZ_SIMPROP_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; diff --git a/src/main/resources/db/migration/V2__update_default_count_value.sql b/src/main/resources/db/migration/V2__update_default_count_value.sql deleted file mode 100644 index 13147a51..00000000 --- a/src/main/resources/db/migration/V2__update_default_count_value.sql +++ /dev/null @@ -1,7 +0,0 @@ --- 파일명: V2__update_default_count_value.sql --- 파일 설명: `bid` 테이블의 `count` 컬럼의 기본값을 3에서 2로 변경 --- 작성일: 2024-10-02 --- 참고: Flyway 명명 규칙 "V<버전번호>__<설명>.sql"에 맞게 작성되었는지 확인해 주세요. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -ALTER TABLE `bid` ALTER COLUMN `count` SET DEFAULT 2; diff --git a/src/main/resources/db/migration/V3__reorder_columns.sql b/src/main/resources/db/migration/V3__reorder_columns.sql deleted file mode 100644 index 15d80688..00000000 --- a/src/main/resources/db/migration/V3__reorder_columns.sql +++ /dev/null @@ -1,66 +0,0 @@ --- 파일명: V3__reorder_columns.sql --- 파일 설명: 가독성을 위해 컬럼 순서 변경 --- 작성일: 2024-10-02 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -ALTER TABLE `users` - CHANGE COLUMN `email` `email` VARCHAR(255) NOT NULL AFTER `nickname`, - CHANGE COLUMN `customer_key` `customer_key` BINARY(16) NOT NULL AFTER `provider_type`, - CHANGE COLUMN `created_at` `created_at` DATETIME(6) NULL DEFAULT NULL AFTER `user_role`, - CHANGE COLUMN `updated_at` `updated_at` DATETIME(6) NULL DEFAULT NULL AFTER `created_at`; - -ALTER TABLE `product` - CHANGE COLUMN `name` `name` VARCHAR(255) NOT NULL AFTER `user_id`, - CHANGE COLUMN `min_price` `min_price` INT NULL DEFAULT NULL AFTER `category`, - CHANGE COLUMN `created_at` `created_at` DATETIME(6) NULL DEFAULT NULL AFTER `min_price`, - CHANGE COLUMN `updated_at` `updated_at` DATETIME(6) NULL DEFAULT NULL AFTER `created_at`; - -ALTER TABLE `auction` - CHANGE COLUMN `product_id` `product_id` BIGINT NULL DEFAULT NULL AFTER `auction_id`, - CHANGE COLUMN `status` `status` VARCHAR(20) NULL DEFAULT NULL AFTER `product_id`, - CHANGE COLUMN `end_date_time` `end_date_time` DATETIME(6) NULL DEFAULT NULL AFTER `status`, - CHANGE COLUMN `winner_id` `winner_id` BIGINT NULL DEFAULT NULL AFTER `end_date_time`; - -ALTER TABLE `image` - CHANGE COLUMN `cdn_path` `cdn_path` VARCHAR(255) NOT NULL AFTER `product_id`, - CHANGE COLUMN `created_at` `created_at` DATETIME(6) NULL DEFAULT NULL AFTER `cdn_path`; - -ALTER TABLE `notification` - CHANGE COLUMN `notification_id` `notification_id` BIGINT NOT NULL AUTO_INCREMENT FIRST, - CHANGE COLUMN `user_id` `user_id` BIGINT NOT NULL AFTER `notification_id`, - CHANGE COLUMN `image_id` `image_id` BIGINT NULL DEFAULT NULL AFTER `user_id`, - CHANGE COLUMN `auction_id` `auction_id` BIGINT NULL DEFAULT NULL AFTER `image_id`, - CHANGE COLUMN `type` `type` VARCHAR(31) NOT NULL AFTER `auction_id`, - CHANGE COLUMN `message` `message` VARCHAR(255) NOT NULL AFTER `type`, - CHANGE COLUMN `is_read` `is_read` BIT(1) NOT NULL AFTER `message`; - -ALTER TABLE `address` - CHANGE COLUMN `user_id` `user_id` BIGINT NOT NULL AFTER `address_id`, - CHANGE COLUMN `zipcode` `zipcode` VARCHAR(255) NULL DEFAULT NULL AFTER `user_id`, - CHANGE COLUMN `road_address` `road_address` VARCHAR(255) NULL DEFAULT NULL AFTER `zipcode`, - CHANGE COLUMN `jibun` `jibun` VARCHAR(255) NULL DEFAULT NULL AFTER `road_address`, - CHANGE COLUMN `detail_address` `detail_address` VARCHAR(255) NULL DEFAULT NULL AFTER `jibun`; - -ALTER TABLE `bank_account` - CHANGE COLUMN `user_id` `user_id` BIGINT NOT NULL AFTER `bank_account_id`, - CHANGE COLUMN `name` `name` VARCHAR(255) NOT NULL AFTER `user_id`, - CHANGE COLUMN `number` `number` VARCHAR(255) NOT NULL AFTER `name`; - -ALTER TABLE `bid` - CHANGE COLUMN `bid_id` `bid_id` BIGINT NOT NULL AUTO_INCREMENT FIRST, - CHANGE COLUMN `user_id` `user_id` BIGINT NOT NULL AFTER `bid_id`, - CHANGE COLUMN `auction_id` `auction_id` BIGINT NOT NULL AFTER `user_id`, - CHANGE COLUMN `amount` `amount` BIGINT NOT NULL AFTER `auction_id`, - CHANGE COLUMN `status` `status` VARCHAR(255) NULL DEFAULT NULL AFTER `count`; - -ALTER TABLE `like_table` - CHANGE COLUMN `user_id` `user_id` BIGINT NOT NULL AFTER `like_id`, - CHANGE COLUMN `created_at` `created_at` DATETIME(6) NULL DEFAULT NULL AFTER `product_id`; - -ALTER TABLE `chzzdb`.`payment` - CHANGE COLUMN `auction_id` `auction_id` BIGINT NOT NULL AFTER `user_id`, - CHANGE COLUMN `amount` `amount` BIGINT NOT NULL AFTER `order_id`, - CHANGE COLUMN `payment_key` `payment_key` VARCHAR(255) NOT NULL AFTER `status`, - CHANGE COLUMN `created_at` `created_at` DATETIME(6) NULL DEFAULT NULL AFTER `payment_key`, - CHANGE COLUMN `updated_at` `updated_at` DATETIME(6) NULL DEFAULT NULL AFTER `created_at`; diff --git a/src/main/resources/db/migration/V4__add_profile_image_url_to_users.sql b/src/main/resources/db/migration/V4__add_profile_image_url_to_users.sql deleted file mode 100644 index 366e52a7..00000000 --- a/src/main/resources/db/migration/V4__add_profile_image_url_to_users.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 파일명: V4__add_profile_image_url_to_users.sql --- 파일 설명: users 테이블에 프로필 이미지 URL(profile_image_url) 컬럼 추가 --- 작성일: 2024-10-05 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -ALTER TABLE `users` - ADD COLUMN `profile_image_url` varchar(255) DEFAULT NULL AFTER `link`; diff --git a/src/main/resources/db/migration/V5__add_image_sequence.sql b/src/main/resources/db/migration/V5__add_image_sequence.sql deleted file mode 100644 index 3659d36e..00000000 --- a/src/main/resources/db/migration/V5__add_image_sequence.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 파일명: V5__add_image_sequence.sql --- 파일 설명: image 테이블에 순서 지정 컬럼 sequence 추가 --- 작성일: 2024-10-05 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -ALTER TABLE `image` - ADD COLUMN `sequence` INT NOT NULL AFTER `cdn_path`; diff --git a/src/main/resources/db/migration/V6__add_address_columns.sql b/src/main/resources/db/migration/V6__add_address_columns.sql deleted file mode 100644 index 9a0860c3..00000000 --- a/src/main/resources/db/migration/V6__add_address_columns.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 파일명: V6__add_address_columns.sql --- 파일 설명: address 테이블에 배송지 주소 관련 컬럼 추가 --- 작성일: 2024-10-20 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - --- 새로운 컬럼 추가 -ALTER TABLE `address` - ADD COLUMN `recipient_name` VARCHAR(255) NOT NULL AFTER `detail_address`, - ADD COLUMN `phone_number` VARCHAR(20) NOT NULL AFTER `recipient_name`, - ADD COLUMN `is_default` BIT(1) NOT NULL DEFAULT 0 AFTER `phone_number`; - --- 기존 필드에 nullable = false 추가 -ALTER TABLE `address` - MODIFY COLUMN `road_address` VARCHAR(255) NOT NULL, - MODIFY COLUMN `jibun` VARCHAR(255) NOT NULL, - MODIFY COLUMN `zipcode` VARCHAR(20) NOT NULL, - MODIFY COLUMN `detail_address` VARCHAR(255) NOT NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V7__add_orders_table.sql b/src/main/resources/db/migration/V7__add_orders_table.sql deleted file mode 100644 index baa97eb7..00000000 --- a/src/main/resources/db/migration/V7__add_orders_table.sql +++ /dev/null @@ -1,30 +0,0 @@ --- 파일명: V7__add_orders_table.sql --- 파일 설명: 주문 테이블 추가 --- 작성일: 2024-10-21 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - --- order 테이블 추가 -CREATE TABLE orders ( - order_id BIGINT NOT NULL AUTO_INCREMENT, - auction_id BIGINT, - buyer_id BIGINT NOT NULL, - payment_id BIGINT NOT NULL, - amount BIGINT NOT NULL, - order_no VARCHAR(255) NOT NULL, - method VARCHAR(30) NOT NULL, - zipcode VARCHAR(255) NOT NULL, - road_address VARCHAR(255) NOT NULL, - jibun VARCHAR(255) NOT NULL, - detail_address VARCHAR(255) NOT NULL, - recipient_name VARCHAR(255) NOT NULL, - phone_number VARCHAR(255) NOT NULL, - delivery_memo VARCHAR(255), - PRIMARY KEY (order_id) -) ENGINE=InnoDB; - - --- auction 테이블과 연관관계 설정 -ALTER TABLE orders - ADD CONSTRAINT FKjn4msbk22y92rmkpf4qa097sv - FOREIGN KEY (auction_id) REFERENCES auction (auction_id); diff --git a/src/main/resources/db/migration/V8__drop_bank_account.sql b/src/main/resources/db/migration/V8__drop_bank_account.sql deleted file mode 100644 index 8a235b65..00000000 --- a/src/main/resources/db/migration/V8__drop_bank_account.sql +++ /dev/null @@ -1,7 +0,0 @@ --- 파일명: V8__drop_bank_account.sql --- 파일 설명: 계좌 번호 테이블 삭제 --- 작성일: 2024-10-23 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -DROP TABLE bank_account; diff --git a/src/main/resources/db/migration/V9__drop_users_link.sql b/src/main/resources/db/migration/V9__drop_users_link.sql deleted file mode 100644 index 8da4cda7..00000000 --- a/src/main/resources/db/migration/V9__drop_users_link.sql +++ /dev/null @@ -1,7 +0,0 @@ --- 파일명: V9__drop_users_link.sql --- 파일 설명: 사용자 테이블 링크 제거 --- 작성일: 2024-10-23 --- 참고: 이 파일은 Flyway 명명 규칙 "V<버전번호>__<설명>.sql"을 따릅니다. --- 적용된 후에는 절대 수정할 수 없으므로, 수정이 필요한 경우에는 새로운 마이그레이션 파일을 작성해 주세요. - -ALTER TABLE users DROP COLUMN link; diff --git a/src/test/java/org/chzz/market/common/TestConfig.java b/src/test/java/org/chzz/market/common/TestConfig.java deleted file mode 100644 index df8522e9..00000000 --- a/src/test/java/org/chzz/market/common/TestConfig.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.chzz.market.common; - -import org.chzz.market.util.AuctionTestFactory; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; - -@TestConfiguration -public class TestConfig { - @Bean - public AuctionTestFactory auctionTestFactory() { - return new AuctionTestFactory(); - } -} \ No newline at end of file diff --git a/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java b/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java index 6207a30f..34e81125 100644 --- a/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java +++ b/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java @@ -1,310 +1,113 @@ package org.chzz.market.domain.auction.controller; -import static java.time.LocalDateTime.now; -import static org.assertj.core.api.Assertions.assertThat; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ALREADY_REGISTERED; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auction.type.AuctionRegisterType.PRE_REGISTER; -import static org.chzz.market.domain.auction.type.AuctionRegisterType.REGISTER; -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.product.entity.Product.Category.ELECTRONICS; -import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; +import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import org.chzz.market.common.AWSConfig; -import org.chzz.market.domain.auction.dto.request.PreRegisterRequest; -import org.chzz.market.domain.auction.dto.request.RegisterAuctionRequest; -import org.chzz.market.domain.auction.dto.request.StartAuctionRequest; -import org.chzz.market.domain.auction.dto.response.PreRegisterResponse; -import org.chzz.market.domain.auction.dto.response.RegisterAuctionResponse; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; -import org.chzz.market.domain.auction.error.AuctionException; -import org.chzz.market.domain.auction.service.AuctionRegistrationServiceFactory; -import org.chzz.market.domain.auction.service.AuctionService; -import org.chzz.market.domain.auction.service.register.AuctionRegisterService; -import org.chzz.market.domain.auction.service.register.PreRegisterService; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.error.exception.UserException; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.chzz.market.domain.auction.dto.AuctionRegisterType; +import org.chzz.market.domain.auction.dto.request.RegisterRequest; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.image.service.ImageService; +import org.chzz.market.util.AuthenticatedRequestTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -@SpringBootTest -@AutoConfigureMockMvc -@Import(AWSConfig.class) -public class AuctionControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private AuctionRegistrationServiceFactory registrationServiceFactory; +class AuctionControllerTest extends AuthenticatedRequestTest { @MockBean - private AuctionService auctionService; - - @Autowired - private ObjectMapper objectMapper; + ImageService imageService; - private MockMultipartFile image1, image2, image3; + RegisterRequest request; - private User user; - private RegisterAuctionRequest registerAuctionRequest; - private PreRegisterRequest preRegisterRequest; - private StartAuctionRequest validStartAuctionRequest, invalidStartAuctionRequest; + MockMultipartFile image1, image2, image3, image4, image5, image6; + MockMultipartFile requestPart; @BeforeEach - void setUp() { - - user = User.builder() - .id(1L) - .email("test@naver.com") - .nickname("테스트 유저") - .build(); - - registerAuctionRequest = RegisterAuctionRequest.builder() - .productName("경매 등록 테스트 상품 이름") - .description("경매 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(REGISTER) - .build(); - - preRegisterRequest = PreRegisterRequest.builder() - .productName("사전 등록 테스트 상품 이름") - .description("사전 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(PRE_REGISTER) - .build(); - - validStartAuctionRequest = StartAuctionRequest.builder() - .productId(1L) - .build(); - - invalidStartAuctionRequest = StartAuctionRequest.builder() - .productId(999L) - .build(); - - image1 = new MockMultipartFile("images", "image1.jpg", "image/jpg", "image1".getBytes()); - image2 = new MockMultipartFile("images", "image2.jpg", "image/jpg", "image2".getBytes()); - image3 = new MockMultipartFile("images", "image3.jpg", "image/jpg", "image3".getBytes()); - System.setProperty("org.mockito.logging.verbosity", "all"); + void setUp() throws JsonProcessingException { + request = new RegisterRequest("name", "description", Category.BOOKS_AND_MEDIA, 10000, + AuctionRegisterType.PRE_REGISTER); + requestPart = new MockMultipartFile( + "request", "request", "application/json", objectMapper.writeValueAsBytes(request) + ); + + image1 = new MockMultipartFile("images", "imagefile1.jpeg", "image/jpeg", + "<>".getBytes()); + image2 = new MockMultipartFile("images", "imagefile2.jpeg", "image/jpeg", + "<>".getBytes()); + + image3 = new MockMultipartFile("images", "imagefile3.jpeg", "image/jpeg", + "<>".getBytes()); + image4 = new MockMultipartFile("images", "imagefile4.jpeg", "image/jpeg", + "<>".getBytes()); + + image5 = new MockMultipartFile("images", "imagefile5.jpeg", "image/jpeg", + "<>".getBytes()); + image6 = new MockMultipartFile("images", "imagefile6.jpeg", "image/gif", + "<>".getBytes()); } - @Nested - @DisplayName("상품 등록 테스트") - class RegisterAuctionTest { - - @Test - @WithMockUser(username = "tester", roles = "USER") - @DisplayName("1. 유효한 요청으로 경매 상품 등록 성공 응답") - void registerAuction_Success() throws Exception { - - String requestJson = objectMapper.writeValueAsString(registerAuctionRequest); - - RegisterAuctionResponse response = RegisterAuctionResponse.of(1L, 1L, PROCEEDING); - - AuctionRegisterService mockService = mock(AuctionRegisterService.class); - when(mockService.register(any(), any(RegisterAuctionRequest.class), anyList())).thenReturn(response); - when(registrationServiceFactory.getService(REGISTER)).thenReturn(mockService); - - MockMultipartFile requestPart = new MockMultipartFile("request", "", "application/json", - requestJson.getBytes()); - - mockMvc.perform(multipart("/api/v1/auctions") - .file(requestPart) - .file(image1).file(image2).file(image3) - .with(csrf()) - .contentType(MediaType.MULTIPART_FORM_DATA)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.productId").value(1)) - .andExpect(jsonPath("$.auctionId").value(1)) - .andExpect(jsonPath("$.status").value("PROCEEDING")) - .andExpect(jsonPath("$.message").value("상품이 성공적으로 경매 등록되었습니다.")); - - verify(mockService).register(any(), any(RegisterAuctionRequest.class), anyList()); - } - - @Test - @WithMockUser(username = "tester", roles = "USER") - @DisplayName("2. 유효한 요청으로 상품 사전 등록 성공 응답") - void preRegisterAuction_Success() throws Exception { - - String requestJson = objectMapper.writeValueAsString(preRegisterRequest); - - PreRegisterResponse response = PreRegisterResponse.of(1L); - - PreRegisterService mockService = mock(PreRegisterService.class); - when(mockService.register(any(), any(PreRegisterRequest.class), anyList())).thenReturn(response); - when(registrationServiceFactory.getService(PRE_REGISTER)).thenReturn(mockService); - - MockMultipartFile requestPart = new MockMultipartFile("request", "", "application/json", - requestJson.getBytes()); - - mockMvc.perform(multipart("/api/v1/auctions") - .file(requestPart) - .file(image1).file(image2).file(image3) - .with(csrf()) - .contentType(MediaType.MULTIPART_FORM_DATA)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.productId").value(1)) - .andExpect(jsonPath("$.auctionId").doesNotExist()) - .andExpect(jsonPath("$.status").doesNotExist()) - .andExpect(jsonPath("$.message").value("상품이 성공적으로 사전 등록되었습니다.")); - - verify(mockService).register(any(), any(PreRegisterRequest.class), anyList()); - } - - @Test - @DisplayName("3. 존재하지 않는 사용자로 경매 상품 등록 실패") - @WithMockUser(username = "tester", roles = "USER") - void registerAuction_UserNotFound() throws Exception { - - Long userId = 999L; - RegisterAuctionRequest invalidRegisterAuctionRequest = RegisterAuctionRequest.builder() - .productName("경매 등록 테스트 상품 이름") - .description("경매 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(REGISTER) - .build(); - - String requestJson = objectMapper.writeValueAsString(invalidRegisterAuctionRequest); - - AuctionRegisterService mockService = mock(AuctionRegisterService.class); - when(mockService.register(any(), any(RegisterAuctionRequest.class), anyList())) - .thenThrow(new UserException(USER_NOT_FOUND)); - when(registrationServiceFactory.getService(REGISTER)).thenReturn(mockService); - - MockMultipartFile requestPart = new MockMultipartFile("request", "", "application/json", - requestJson.getBytes()); + @Test + @DisplayName("사전 경매 등록") + void testPreAuctionRegistration() throws Exception { + // when + mockMvc.perform(multipart("/api/v1/auctions") + .file(requestPart) + .file(image1) + .file(image2) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isCreated()) + .andDo(print()); - mockMvc.perform(multipart("/api/v1/auctions") - .file(requestPart) - .file(image1).file(image2).file(image3) - .with(csrf()) - .contentType(MediaType.MULTIPART_FORM_DATA)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.message").value("사용자를 찾을 수 없습니다.")); - } + verify(imageService).uploadImages(any()); } - @Nested - @DisplayName("사전 등록 된 상품 경매 등록 상품으로 전환 테스트") - class ConvertToAuctionTest { - - @Test - @DisplayName("1. 유효한 요청으로 사전 등록 된 상품 경매 등록 전환 성공 응답") - @WithMockUser(username = "tester", roles = "USER") - void convertToAuction_Success() throws Exception { - - String requestJson = objectMapper.writeValueAsString(validStartAuctionRequest); - - StartAuctionResponse response = StartAuctionResponse.of(1L, 1L, PROCEEDING, - LocalDateTime.now().plusDays(1)); - when(auctionService.startAuction(any(), any(StartAuctionRequest.class))).thenReturn(response); - - mockMvc.perform(post("/api/v1/auctions/start") - .content(requestJson) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.auctionId").value(1)) - .andExpect(jsonPath("$.productId").value(1)) - .andExpect(jsonPath("$.status").value("PROCEEDING")) - .andExpect(jsonPath("$.endDateTime").isNotEmpty()) - .andExpect(jsonPath("$.message").value("경매가 성공적으로 시작되었습니다.")); - - verify(auctionService).startAuction(any(), any(StartAuctionRequest.class)); - } - - @Test - @DisplayName("2. 존재하지 않는 상품 ID로 전환 시도 실패") - @WithMockUser(username = "tester", roles = "USER") - void convertToAuction_NotFound() throws Exception { - - String requestJson = objectMapper.writeValueAsString(invalidStartAuctionRequest); - - when(auctionService.startAuction(any(), any(StartAuctionRequest.class))) - .thenThrow(new AuctionException(AUCTION_NOT_FOUND)); - - mockMvc.perform(post("/api/v1/auctions/start") - .content(requestJson) - .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.message").value("경매를 찾을 수 없습니다.")); - } - - @Test - @DisplayName("3. 이미 경매 중인 상품 전환 시도 실패") - @WithMockUser(username = "tester", roles = "USER") - void convertToAuction_AlreadyInAuction() throws Exception { - - String requestJson = objectMapper.writeValueAsString(validStartAuctionRequest); - - when(auctionService.startAuction(any(), any(StartAuctionRequest.class))) - .thenThrow(new AuctionException(AUCTION_ALREADY_REGISTERED)); + @Test + @DisplayName("이미지가 없는 경우") + void testRegisterAuctionWithNoImage() throws Exception { + + MockMultipartFile emptyImage = new MockMultipartFile( + "images", "file", MediaType.MULTIPART_FORM_DATA_VALUE, new byte[0] + ); + // when + mockMvc.perform(multipart("/api/v1/auctions") + .file(emptyImage) + .file(requestPart) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message[0]").value(containsString("images: 파일은 최소 하나 이상 필요합니다."))) + .andDo(print()); - mockMvc.perform(post("/api/v1/auctions/start") - .content(requestJson) - .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("이미 등록된 경매입니다.")); - } } @Test - @DisplayName("4. 전환 후 상태와 시간 정보 확인") - @WithMockUser(username = "tester", roles = "USER") - void convertToAuction_CheckStateAndTime() throws Exception { - Long productId = 1L; - LocalDateTime startTime = now(); - LocalDateTime endTime = startTime.plusHours(24); - - StartAuctionResponse response = StartAuctionResponse.of(1L, productId, PROCEEDING, endTime); - when(auctionService.startAuction(any(), any(StartAuctionRequest.class))).thenReturn(response); - - String requestJson = objectMapper.writeValueAsString(validStartAuctionRequest); - - MvcResult result = mockMvc.perform(post("/api/v1/auctions/start") - .content(requestJson) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.auctionId").value(1)) - .andExpect(jsonPath("$.productId").value(productId)) - .andExpect(jsonPath("$.status").value("PROCEEDING")) - .andExpect(jsonPath("$.endDateTime").isNotEmpty()) - .andExpect(jsonPath("$.message").value("경매가 성공적으로 시작되었습니다.")) - .andReturn(); - - String content = result.getResponse().getContentAsString(); - StartAuctionResponse returnedResponse = objectMapper.readValue(content, StartAuctionResponse.class); - - assertThat(returnedResponse.endDateTime()).isAfter(startTime); - assertThat(returnedResponse.endDateTime()).isBefore(startTime.plusHours(25)); - assertThat(ChronoUnit.HOURS.between(startTime, returnedResponse.endDateTime())).isEqualTo(24); + @DisplayName("이미지가 5개 이상인 경우") + void testRegisterAuctionWithOverImageCount() throws Exception { + // given + mockMvc.perform(multipart("/api/v1/auctions") + .file(requestPart) + .file(image1) + .file(image2) + .file(image3) + .file(image4) + .file(image5) + .file(image6) + .accept(MediaType.APPLICATION_JSON)) + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message[0]").value(containsString("images: 이미지는 5장 이내로만 업로드 가능합니다."))) + .andDo(print()); } } diff --git a/src/test/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequestTest.java b/src/test/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequestTest.java deleted file mode 100644 index 16e657d6..00000000 --- a/src/test/java/org/chzz/market/domain/auction/dto/request/BaseRegisterRequestTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.chzz.market.domain.auction.dto.request; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.regex.Pattern; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -public class BaseRegisterRequestTest { - - private static final String DESCRIPTION_REGEX = "^(?:(?:[^\\n]*\\n){0,10}[^\\n]*$)"; // 개행문자 10개를 제한 - private static final Pattern pattern = Pattern.compile(DESCRIPTION_REGEX); - - @DisplayName("정규식 - 매칭이 되어야 하는 경우") - @ParameterizedTest(name = "{index} - 문자열 매칭: {0}") - @ValueSource(strings = { - "", // 빈 문자열 - " ", // 공백만 있는 경우 - "This is a simple string with no newline.", // 개행 없는 짧은 문자열 - "Hello\nWorld\nThis\nis\na\ntest\nstring.", // 개행 6번 포함 - "SingleLine", // 개행 없이 10자 미만 - "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8\nLine9\nLine10", // 개행 9번 - "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8\nLine9\nLine10\n", //개행 10번 마지막 개행 - "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8\nLine9\nLine10\nLine11", //개행 10번 - "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8\nLine9\nLine10\n ", //개행 10번 + 마지막 띄어쓰기 - "\n", //개행문자 1번 - "\n\n\n\n\n\n\n\n\n\n" //개행만 10번 - }) - void testDescriptionRegexMatches(String input) { - // 매칭이 되어야 함 - assertThat(pattern.matcher(input).matches()).isTrue(); - } - - @DisplayName("정규식 - 매칭이 되지 않아야 하는 경우") - @ParameterizedTest(name = "{index} - 문자열 매칭 실패 예상: {0}") - @ValueSource(strings = { - "Exceeding\nnewlines\nlimit\nby\nadding\nextra\nnewlines\nhere\nbeyond\nallowed\nlimit.\n.", // 개행 11번 - "Exceeding\nnewlines\nlimit\nby\nadding\nextra\nnewlines\nhere\nbeyond\nallowed\nlimit.\n", // 개행 11번 - }) - void testDescriptionRegexDoesNotMatch(String input) { - // 매칭이 되지 않아야 함 - assertThat(pattern.matcher(input).matches()).isFalse(); - } -} - diff --git a/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java b/src/test/java/org/chzz/market/domain/auction/entity/AuctionTest.java similarity index 75% rename from src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java rename to src/test/java/org/chzz/market/domain/auction/entity/AuctionTest.java index 8d7a187f..1c5db417 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/entity/AuctionV2Test.java +++ b/src/test/java/org/chzz/market/domain/auction/entity/AuctionTest.java @@ -1,25 +1,25 @@ -package org.chzz.market.domain.auctionv2.entity; +package org.chzz.market.domain.auction.entity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_ENDED; -import static org.chzz.market.domain.imagev2.error.ImageErrorCode.IMAGE_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ALREADY_OFFICIAL; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_ENDED; +import static org.chzz.market.domain.image.error.ImageErrorCode.IMAGE_NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.util.List; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.image.entity.ImageV2; -import org.chzz.market.domain.imagev2.error.exception.ImageException; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.image.entity.Image; +import org.chzz.market.domain.image.error.exception.ImageException; import org.chzz.market.domain.user.entity.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class AuctionV2Test { +class AuctionTest { private static final String ERROR_CODE = "errorCode"; - private AuctionV2 auction; + private Auction auction; private User owner; private User otherUser; @@ -32,7 +32,7 @@ void setUp() { .id(2L) .build(); - auction = AuctionV2.builder() + auction = Auction.builder() .seller(owner) .status(AuctionStatus.PRE) .build(); @@ -70,11 +70,11 @@ void setUp() { @Test void 첫번째이미지를_정상적으로_반환한다() { - ImageV2 firstImage = ImageV2.builder() + Image firstImage = Image.builder() .cdnPath("cdn/path/to/first_image.jpg") .sequence(1) .build(); - ImageV2 secondImage = ImageV2.builder() + Image secondImage = Image.builder() .cdnPath("cdn/path/to/second_image.jpg") .sequence(2) .build(); @@ -92,7 +92,7 @@ void setUp() { @Test void 낙찰자가_맞는경우() { - AuctionV2 winnerAuction = AuctionV2.builder() + Auction winnerAuction = Auction.builder() .seller(owner) .status(AuctionStatus.PRE) .winnerId(owner.getId()) @@ -107,7 +107,7 @@ void setUp() { @Test void 낙찰자가_아닐때_조회하는경우_false_반환() { - AuctionV2 winnerAuction = AuctionV2.builder() + Auction winnerAuction = Auction.builder() .seller(owner) .status(AuctionStatus.PRE) .winnerId(owner.getId()) @@ -117,9 +117,9 @@ void setUp() { @Test void 아직_경매가_끝나지_않을때_예외가_발생한다() { - List auctions = List.of( - AuctionV2.builder().seller(owner).status(AuctionStatus.PRE).winnerId(owner.getId()).build(), - AuctionV2.builder().seller(owner).status(AuctionStatus.PROCEEDING).winnerId(owner.getId()).build() + List auctions = List.of( + Auction.builder().seller(owner).status(AuctionStatus.PRE).winnerId(owner.getId()).build(), + Auction.builder().seller(owner).status(AuctionStatus.PROCEEDING).winnerId(owner.getId()).build() ); auctions.forEach(auction -> assertThatThrownBy(auction::validateAuctionEnded) diff --git a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java similarity index 72% rename from src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java rename to src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java index fa148a73..cf4536dd 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/repository/AuctionV2QueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java @@ -1,30 +1,30 @@ -package org.chzz.market.domain.auctionv2.repository; +package org.chzz.market.domain.auction.repository; import static org.assertj.core.api.Assertions.assertThat; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; -import org.chzz.market.domain.auctionv2.dto.response.EndedAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionDetailResponse; -import org.chzz.market.domain.auctionv2.dto.response.OfficialAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.PreAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.ProceedingAuctionResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auctionv2.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.entity.Category; +import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; +import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; +import org.chzz.market.domain.auction.dto.response.OfficialAuctionDetailResponse; +import org.chzz.market.domain.auction.dto.response.OfficialAuctionResponse; +import org.chzz.market.domain.auction.dto.response.PreAuctionResponse; +import org.chzz.market.domain.auction.dto.response.ProceedingAuctionResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; +import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.entity.Bid.BidStatus; import org.chzz.market.domain.bid.repository.BidRepository; -import org.chzz.market.domain.image.entity.ImageV2; -import org.chzz.market.domain.likev2.entity.LikeV2; -import org.chzz.market.domain.likev2.repository.LikeV2Repository; -import org.chzz.market.domain.orderv2.entity.OrderV2; -import org.chzz.market.domain.orderv2.repository.OrderV2Repository; -import org.chzz.market.domain.paymentv2.entity.PaymentV2.PaymentMethod; +import org.chzz.market.domain.image.entity.Image; +import org.chzz.market.domain.like.entity.Like; +import org.chzz.market.domain.like.repository.LikeRepository; +import org.chzz.market.domain.order.entity.Order; +import org.chzz.market.domain.order.repository.OrderRepository; +import org.chzz.market.domain.payment.entity.Payment.PaymentMethod; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; @@ -41,37 +41,37 @@ @SpringBootTest @Transactional -class AuctionV2QueryRepositoryTest { +class AuctionQueryRepositoryTest { @Autowired - private AuctionV2QueryRepository auctionQueryRepository; + private AuctionQueryRepository auctionQueryRepository; @Autowired - private AuctionV2Repository auctionV2Repository; + private AuctionRepository auctionRepository; @Autowired private BidRepository bidRepository; @Autowired private UserRepository userRepository; @Autowired - private OrderV2Repository orderRepository; + private OrderRepository orderRepository; @Autowired - private LikeV2Repository likeV2Repository; + private LikeRepository likeRepository; private User seller; private User user, user1; - private ImageV2 defaultImage; + private Image defaultImage; @BeforeEach void setUp() { seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); user = User.builder().email("user").providerId("user").providerType(User.ProviderType.KAKAO).build(); user1 = User.builder().email("user1").providerId("user1").providerType(User.ProviderType.KAKAO).build(); - defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + defaultImage = Image.builder().cdnPath("https://cdn.com").sequence(1).build(); userRepository.saveAll(List.of(seller, user, user1)); } - private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId, - Integer minPrice) { - AuctionV2 auction = AuctionV2.builder() + private Auction createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId, + Integer minPrice) { + Auction auction = Auction.builder() .seller(seller) .name(name) .description(description) @@ -81,11 +81,11 @@ private AuctionV2 createAuction(User seller, String name, String description, Au .minPrice(minPrice) .build(); auction.addImage(defaultImage); - auctionV2Repository.save(auction); + auctionRepository.save(auction); return auction; } - private Bid createBid(User bidder, AuctionV2 auction, Long amount, Bid.BidStatus status) { + private Bid createBid(User bidder, Auction auction, Long amount, Bid.BidStatus status) { Bid bid = Bid.builder() .bidderId(bidder.getId()) .auctionId(auction.getId()) @@ -96,8 +96,8 @@ private Bid createBid(User bidder, AuctionV2 auction, Long amount, Bid.BidStatus return bid; } - private OrderV2 createOrder(AuctionV2 auction, User buyer, Long amount) { - OrderV2 order = OrderV2.builder() + private Order createOrder(Auction auction, User buyer, Long amount) { + Order order = Order.builder() .auction(auction) .buyerId(buyer.getId()) .amount(amount) @@ -118,7 +118,7 @@ private OrderV2 createOrder(AuctionV2 auction, User buyer, Long amount) { @Test void 낙찰정보를_조회한다() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, user.getId(), + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, user.getId(), 1000); createBid(user, auction, 2000L, Bid.BidStatus.ACTIVE); @@ -136,7 +136,7 @@ class OfficialAuctionDetail { @Test void 본인의_제품을_조회한경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, seller.getId(), + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, seller.getId(), 1000); // When @@ -155,7 +155,7 @@ class OfficialAuctionDetail { @Test void 다른_사람_경매를_참여안한경우_조회한경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); // When Optional result = auctionQueryRepository.findOfficialAuctionDetailById( @@ -171,7 +171,7 @@ class OfficialAuctionDetail { @Test void 다른_사람_경매을_참여한경우_조회() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); createBid(user, auction, 1000L, Bid.BidStatus.ACTIVE); // When @@ -198,7 +198,7 @@ class OfficialAuctionDetail { @Test void 비로그인_상태에서_조회한_경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); // When Optional result = auctionQueryRepository.findOfficialAuctionDetailById( @@ -217,7 +217,7 @@ class OfficialAuctionDetail { @Test void 취소된_입찰이_있는_경우() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); createBid(user, auction, 2000L, Bid.BidStatus.CANCELLED); // When @@ -233,7 +233,7 @@ class OfficialAuctionDetail { @Test void 주문이_있을시_조회를_한다() { // Given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, user.getId(), 2000); createBid(user, auction, 2000L, Bid.BidStatus.ACTIVE); createOrder(auction, user, 2000L); @@ -259,9 +259,9 @@ class Auctions { @Test public void 정식경매_목록_조회_테스트_본인것() throws Exception { //given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); - auctionV2Repository.save(auction); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + auctionRepository.save(auction); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); //when Page result = auctionQueryRepository.findOfficialAuctions(seller.getId(), @@ -276,9 +276,9 @@ class Auctions { @Test public void 정식경매_목록_조회_테스트_남의것() throws Exception { //given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); - auctionV2Repository.save(auction); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + auctionRepository.save(auction); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); //when Page result = auctionQueryRepository.findOfficialAuctions(user.getId(), @@ -304,9 +304,9 @@ class Auctions { @Test public void 정식경매_목록_조회_테스트_입찰을했을때() throws Exception { //given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); - auctionV2Repository.save(auction); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); + auctionRepository.save(auction); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); Bid bid = createBid(user, auction, 1000L, BidStatus.ACTIVE); bidRepository.save(bid); //when @@ -323,13 +323,13 @@ class Auctions { @Test public void 사전경매_목록조회_좋아요를_했을때() { //given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); - auctionV2Repository.save(auction); - LikeV2 like = LikeV2.builder().auctionId(auction.getId()).userId(user.getId()).build(); - likeV2Repository.save(like); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + auctionRepository.save(auction); + Like like = Like.builder().auctionId(auction.getId()).userId(user.getId()).build(); + likeRepository.save(like); //when - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); Page result = auctionQueryRepository.findPreAuctions(user.getId(), Category.ELECTRONICS, pageable); //then @@ -343,11 +343,11 @@ class Auctions { @Test public void 사전경매_목록조회_좋아요를_안했을때() { //given - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); - auctionV2Repository.save(auction); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + auctionRepository.save(auction); //when - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); Page resultWithUserId = auctionQueryRepository.findPreAuctions(user.getId(), Category.ELECTRONICS, pageable); @@ -371,14 +371,14 @@ class Auctions { @Test public void 정식경매_목록_조회_정렬_테스트() throws Exception { //given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, 2000); - auctionV2Repository.save(auction1); - auctionV2Repository.save(auction2); + auctionRepository.save(auction1); + auctionRepository.save(auction2); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); //when Page result = auctionQueryRepository.findOfficialAuctions(seller.getId(), @@ -394,7 +394,7 @@ class Auctions { @Test public void 정식경매_목록_조회_종료까지_남은시간_테스트() throws Exception { // given - AuctionV2 auction1 = AuctionV2.builder() + Auction auction1 = Auction.builder() .seller(seller) .name("맥북프로") .description("맥북프로 2019년형 팝니다.") @@ -405,7 +405,7 @@ class Auctions { .endDateTime(LocalDateTime.now().plusSeconds(3600)) // 1시간 뒤 종료 .build(); - AuctionV2 auction2 = AuctionV2.builder() + Auction auction2 = Auction.builder() .seller(seller) .name("아이패드") .description("아이패드 2019년형 팝니다.") @@ -415,8 +415,8 @@ class Auctions { .minPrice(2000) .endDateTime(LocalDateTime.now().plusSeconds(7200)) // 2시간 뒤 종료 .build(); - auctionV2Repository.saveAll(List.of(auction1, auction2)); - Pageable pageable = PageRequest.of(0, 10, Sort.by("immediately-v2")); + auctionRepository.saveAll(List.of(auction1, auction2)); + Pageable pageable = PageRequest.of(0, 10, Sort.by("immediately")); Page resultWithin1Hour = auctionQueryRepository.findOfficialAuctions(null, null, AuctionStatus.PROCEEDING, 3600, pageable); @@ -449,17 +449,17 @@ class MyAuctions { @Test void 내가_좋아요한_사전경매_목록조회() { // Given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); - auctionV2Repository.save(auction1); - auctionV2Repository.save(auction2); + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionRepository.save(auction1); + auctionRepository.save(auction2); - LikeV2 like1 = LikeV2.builder().auctionId(auction1.getId()).userId(user.getId()).build(); - LikeV2 like2 = LikeV2.builder().auctionId(auction2.getId()).userId(user.getId()).build(); - likeV2Repository.save(like1); - likeV2Repository.save(like2); + Like like1 = Like.builder().auctionId(auction1.getId()).userId(user.getId()).build(); + Like like2 = Like.builder().auctionId(auction2.getId()).userId(user.getId()).build(); + likeRepository.save(like1); + likeRepository.save(like2); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); // When Page result = auctionQueryRepository.findLikedAuctionsByUserId(user.getId(), pageable); @@ -475,12 +475,12 @@ class MyAuctions { @Test void 사용자가_등록한_사전경매_목록_조회() { // Given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); - AuctionV2 auction3 = createAuction(user, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); - auctionV2Repository.saveAll(List.of(auction1, auction2, auction3)); + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PRE, null, 1000); + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + Auction auction3 = createAuction(user, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); // When Page result = auctionQueryRepository.findPreAuctionsByUserId(seller.getId(), pageable); @@ -503,16 +503,16 @@ class MyAuctions { @Test void 사용자가_등록한_진행중인_경매_목록_조회() { // Given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null, 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, 2000); - AuctionV2 auction3 = createAuction(user, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.PROCEEDING, null, 1500); - AuctionV2 auction4 = createAuction(seller, "종료 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, null, 2000); - AuctionV2 auction5 = createAuction(seller, "사전 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); - auctionV2Repository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5)); + Auction auction3 = createAuction(user, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.PROCEEDING, null, 1500); + Auction auction4 = createAuction(seller, "종료 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, null, 2000); + Auction auction5 = createAuction(seller, "사전 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionRepository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5)); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); // When Page result = auctionQueryRepository.findProceedingAuctionsByUserId( @@ -534,18 +534,18 @@ class MyAuctions { @Test void 사용자가_등록한_종료된_경매_목록_조회() { // Given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, null, 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user.getId(), + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, null, 1000); + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user.getId(), 2000); - AuctionV2 auction3 = createAuction(user, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.ENDED, null, 1500); - AuctionV2 auction4 = createAuction(seller, "진행중 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, + Auction auction3 = createAuction(user, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.ENDED, null, 1500); + Auction auction4 = createAuction(seller, "진행중 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PROCEEDING, null, 2000); - AuctionV2 auction5 = createAuction(seller, "사전 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); - auctionV2Repository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5)); + Auction auction5 = createAuction(seller, "사전 아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.PRE, null, 2000); + auctionRepository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5)); createOrder(auction2, user, 2000L); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); // When Page result = auctionQueryRepository.findEndedAuctionsByUserId( @@ -571,19 +571,19 @@ class MyAuctions { @Test void 사용자가_낙찰한_경매_목록_조회() { // given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, user.getId(), + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, user.getId(), 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user.getId(), + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user.getId(), 2000); - AuctionV2 auction3 = createAuction(seller, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.ENDED, null, 1500); - auctionV2Repository.saveAll(List.of(auction1, auction2, auction3)); + Auction auction3 = createAuction(seller, "갤럭시탭", "갤럭시탭 S7 팝니다.", AuctionStatus.ENDED, null, 1500); + auctionRepository.saveAll(List.of(auction1, auction2, auction3)); // 사용자의 입찰 생성 createBid(user, auction1, 2000L, Bid.BidStatus.ACTIVE); createBid(user, auction2, 3000L, Bid.BidStatus.ACTIVE); createOrder(auction1, user, 2000L); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); // When Page result = auctionQueryRepository.findWonAuctionsByUserId( @@ -607,11 +607,11 @@ class MyAuctions { @Test void 사용자가_낙찰_실패한_경매_목록_조회() { // given - AuctionV2 auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, user1.getId(), + Auction auction1 = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.ENDED, user1.getId(), 1000); - AuctionV2 auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user1.getId(), + Auction auction2 = createAuction(seller, "아이패드", "아이패드 2021년형 팝니다.", AuctionStatus.ENDED, user1.getId(), 2000); - auctionV2Repository.saveAll(List.of(auction1, auction2)); + auctionRepository.saveAll(List.of(auction1, auction2)); // 사용자의 입찰 생성 (하지만 낙찰되지 않음) createBid(user, auction1, 1000L, Bid.BidStatus.ACTIVE); @@ -619,7 +619,7 @@ class MyAuctions { createBid(user, auction2, 2000L, Bid.BidStatus.ACTIVE); createBid(user1, auction2, 3000L, Bid.BidStatus.ACTIVE); - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive-v2")); + Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); // when Page result = auctionQueryRepository.findLostAuctionsByUserId( diff --git a/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java b/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java deleted file mode 100644 index 1c2489c5..00000000 --- a/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java +++ /dev/null @@ -1,808 +0,0 @@ -package org.chzz.market.domain.auction.repository; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.chzz.market.domain.auction.type.AuctionStatus.ENDED; -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.payment.entity.Status.DONE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.time.LocalDateTime; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.chzz.market.common.DatabaseTest; -import org.chzz.market.domain.auction.dto.BaseAuctionDto; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.AuctionResponse; -import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserAuctionResponse; -import org.chzz.market.domain.auction.dto.response.UserEndedAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.bid.entity.Bid; -import org.chzz.market.domain.bid.entity.Bid.BidStatus; -import org.chzz.market.domain.bid.repository.BidRepository; -import org.chzz.market.domain.delivery.entity.Delivery; -import org.chzz.market.domain.image.dto.ImageResponse; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.repository.ImageRepository; -import org.chzz.market.domain.order.entity.Order; -import org.chzz.market.domain.order.repository.OrderRepository; -import org.chzz.market.domain.payment.dto.response.TossPaymentResponse; -import org.chzz.market.domain.payment.entity.Payment; -import org.chzz.market.domain.payment.entity.Payment.PaymentMethod; -import org.chzz.market.domain.payment.repository.PaymentRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.dto.response.ParticipationCountsResponse; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; -import org.springframework.transaction.annotation.Transactional; - -@DatabaseTest -class AuctionRepositoryCustomImplTest { - - @Autowired - AuctionRepository auctionRepository; - - private User user1, user2, user3, user4, user5; - private Product product1, product2, product3, product4, product5, product6, product7, product8, product9, product10; - private Auction auction1, auction2, auction3, auction4, auction5, auction6, auction7, auction8, auction9, auction10; - - private Image image1, image2, image3, image4, image5, image6; - private Bid bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8, bid9, bid10, bid11, bid12, bid13, bid14, bid15; - private Order order1; - - @BeforeEach - void init(@Autowired UserRepository userRepository, - @Autowired ProductRepository productRepository, - @Autowired AuctionRepository auctionRepository, - @Autowired ImageRepository imageRepository, - @Autowired BidRepository bidRepository, - @Autowired PaymentRepository paymentRepository, - @Autowired OrderRepository orderRepository) { - user1 = User.builder().providerId("1234").nickname("닉네임1").email("asd@naver.com").build(); - user2 = User.builder().providerId("12345").nickname("닉네임2").email("asd1@naver.com").build(); - user3 = User.builder().providerId("123456").nickname("닉네임3").email("asd12@naver.com").build(); - user4 = User.builder().providerId("1234567").nickname("닉네임4").email("asd123@naver.com").build(); - user5 = User.builder().providerId("12345678").nickname("닉네임5").email("asd1234@naver.com").build(); - userRepository.saveAll(List.of(user1, user2, user3, user4, user5)); - - product1 = Product.builder().user(user1).name("제품1").category(Category.FASHION_AND_CLOTHING).minPrice(10000) - .build(); - product2 = Product.builder().user(user1).name("제품2").category(Category.BOOKS_AND_MEDIA).minPrice(20000) - .build(); - product3 = Product.builder().user(user2).name("제품3").category(Category.FASHION_AND_CLOTHING).minPrice(30000) - .build(); - product4 = Product.builder().user(user2).name("제품4").category(Category.FASHION_AND_CLOTHING).minPrice(40000) - .build(); - product5 = Product.builder().user(user2).name("제품5").category(Category.ELECTRONICS).minPrice(50000) - .build(); - product6 = Product.builder().user(user2).name("제품6").category(Category.FURNITURE_AND_INTERIOR).minPrice(60000) - .build(); - product7 = Product.builder().user(user2).name("제품7").category(Category.SPORTS_AND_LEISURE).minPrice(70000) - .build(); - product8 = Product.builder().user(user2).name("제품8").category(Category.OTHER).minPrice(75000) - .build(); - product9 = Product.builder().user(user3).name("제품9").category(Category.OTHER).minPrice(75000) - .build(); - product10 = Product.builder().user(user2).name("제품10").category(Category.OTHER).minPrice(80000) - .build(); - - productRepository.saveAll( - List.of(product1, product2, product3, product4, product5, product6, product7, product8, product9, - product10)); - - auction1 = Auction.builder().product(product1).status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - auction2 = Auction.builder().product(product2).status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - auction3 = Auction.builder().product(product3).status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - auction4 = Auction.builder().product(product4).status(ENDED).winnerId(user3.getId()) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - auction5 = Auction.builder().product(product5).status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusHours(1)).build(); - auction6 = Auction.builder().product(product6).status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusSeconds(3000)).build(); - auction7 = Auction.builder().product(product7).status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusSeconds(700)).build(); - auction8 = Auction.builder().product(product8).status(ENDED).winnerId(user4.getId()) - .endDateTime(LocalDateTime.now().minusDays(1)).build(); - auction9 = Auction.builder().product(product9).status(PROCEEDING).winnerId(null) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - // auction10 생성 (종료되었지만 낙찰자가 없는 경매) - auction10 = Auction.builder().product(product10).status(ENDED) - .endDateTime(LocalDateTime.now().minusDays(1)).build(); - auctionRepository.saveAll( - List.of(auction1, auction2, auction3, auction4, auction5, auction6, auction7, auction8, auction9, - auction10)); - // auction8에 대한 결제 데이터 추가 (결제 완료된 경매) - TossPaymentResponse tossPaymentResponse = new TossPaymentResponse(); - tossPaymentResponse.setTotalAmount(250000L); - tossPaymentResponse.setMethod(PaymentMethod.CARD); - tossPaymentResponse.setStatus(DONE); - tossPaymentResponse.setOrderId("order_" + auction8.getId()); - tossPaymentResponse.setPaymentKey("paymentKey_" + auction8.getId()); - Payment payment1 = Payment.of(user4, tossPaymentResponse, auction8); - paymentRepository.save(payment1); - - image1 = Image.builder().product(product1).cdnPath("path/to/image1_1.jpg").sequence(1).build(); - image2 = Image.builder().product(product1).cdnPath("path/to/image1_2.jpg").sequence(2).build(); - image3 = Image.builder().product(product2).cdnPath("path/to/image2.jpg").sequence(1).build(); - image4 = Image.builder().product(product3).cdnPath("path/to/image3.jpg").sequence(1).build(); - image5 = Image.builder().product(product4).cdnPath("path/to/image4.jpg").sequence(1).build(); - image6 = Image.builder().product(product8).cdnPath("path/to/image5.jpg").sequence(1).build(); - imageRepository.saveAll(List.of(image1, image2, image3, image4, image5, image6)); - - bid1 = Bid.builder().bidderId(user2.getId()).auctionId(auction1.getId()).status(BidStatus.ACTIVE).amount(2000L) - .build(); - bid2 = Bid.builder().bidderId(user2.getId()).auctionId(auction2.getId()).status(BidStatus.ACTIVE).amount(4000L) - .build(); - bid3 = Bid.builder().bidderId(user1.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE).amount(5000L) - .build(); - bid4 = Bid.builder().bidderId(user3.getId()).auctionId(auction2.getId()).status(BidStatus.ACTIVE).amount(6000L) - .build(); - bid5 = Bid.builder().bidderId(user1.getId()).auctionId(auction5.getId()).status(BidStatus.ACTIVE).amount(7000L) - .build(); - bid6 = Bid.builder().bidderId(user2.getId()).auctionId(auction6.getId()).status(BidStatus.ACTIVE).amount(8000L) - .build(); - bid7 = Bid.builder().bidderId(user3.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE) - .amount(310000L).build(); - bid8 = Bid.builder().bidderId(user4.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE) - .amount(320000L).build(); - bid10 = Bid.builder().bidderId(user2.getId()).auctionId(auction3.getId()).status(BidStatus.ACTIVE).amount(8000L) - .build(); - bid11 = Bid.builder().bidderId(user2.getId()).auctionId(auction4.getId()).status(BidStatus.ACTIVE) - .amount(15000L).build(); - bid12 = Bid.builder().bidderId(user3.getId()).auctionId(auction4.getId()).status(BidStatus.ACTIVE) - .amount(25000L).build(); - bid13 = Bid.builder().bidderId(user4.getId()).auctionId(auction8.getId()).status(BidStatus.ACTIVE) - .amount(250000L).build(); - bid14 = Bid.builder().bidderId(user2.getId()).auctionId(auction8.getId()).status(BidStatus.ACTIVE) - .amount(150000L).build(); - bid15 = Bid.builder().bidderId(user5.getId()).auctionId(auction9.getId()).amount(75000L) - .status(BidStatus.ACTIVE).build(); - - bid15.cancelBid(); - bidRepository.saveAll(List.of(bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8, bid10, bid11, bid12, bid13, - bid14, bid15)); - - Delivery delivery = Delivery.builder() - .roadAddress("서울시 강남구") - .jibun("12345") - .zipcode("06000") - .detailAddress("101동 202호") - .recipientName("홍길동") - .phoneNumber("010-1234-5678") - .build(); - - order1 = Order.of(4L, payment1, delivery, "부재시 경비실에 맡겨주세요."); - orderRepository.save(order1); - } - - @Test - @DisplayName("특정 카테고리 경매를 높은 가격순으로 조회") - public void testFindAuctionsByCategoryExpensive() throws Exception { - //given - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); - - //when - Page result = auctionRepository.findAuctionsByCategory( - Category.FASHION_AND_CLOTHING, user1.getId(), pageable); - - //then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("제품3"); - assertThat(result.getContent().get(0).getIsParticipated()).isTrue(); - assertThat(result.getContent().get(0).getParticipantCount()).isEqualTo(4); - assertThat(result.getContent().get(0).getImageUrl()).isEqualTo("path/to/image3.jpg"); - assertThat(result.getContent().get(1).getProductName()).isEqualTo("제품1"); - assertThat(result.getContent().get(1).getIsParticipated()).isFalse(); - assertThat(result.getContent().get(1).getParticipantCount()).isEqualTo(1); - assertThat(result.getContent().get(1).getImageUrl()).isEqualTo("path/to/image1_1.jpg"); - } - - @Test - @DisplayName("특정 카테고리 경매를 인기순으로 조회") - public void testFindAuctionsByCategoryPopularity() throws Exception { - //given - Pageable pageable = PageRequest.of(0, 10, Sort.by("popularity")); - - //when - Page result = auctionRepository.findAuctionsByCategory( - Category.FASHION_AND_CLOTHING, user2.getId(), pageable); - - //then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("제품3"); - assertThat(result.getContent().get(0).getIsParticipated()).isTrue(); - assertThat(result.getContent().get(0).getParticipantCount()).isEqualTo(4); - assertThat(result.getContent().get(0).getImageUrl()).isEqualTo("path/to/image3.jpg"); - assertThat(result.getContent().get(1).getProductName()).isEqualTo("제품1"); - assertThat(result.getContent().get(1).getIsParticipated()).isTrue(); - assertThat(result.getContent().get(1).getParticipantCount()).isEqualTo(1); - assertThat(result.getContent().get(1).getImageUrl()).isEqualTo("path/to/image1_1.jpg"); - } - - @Test - @DisplayName("경매가 없는 경우 조회") - public void testFindAuctionsByCategoryNoAuctions() throws Exception { - //given - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); - - //when - Page result = auctionRepository.findAuctionsByCategory( - Category.TOYS_AND_HOBBIES, 1L, pageable); - - //then - assertThat(result).isNotNull(); - assertThat(result.getContent()).isEmpty(); - } - - @Test - @DisplayName("경매 상세 조회 - 본인의 제품 경매인 경우") - public void testFindAuctionDetailsById() throws Exception { - //given - Long auctionId = auction1.getId(); - Long userId = user1.getId(); - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - - //then - assertThat(result).isPresent(); - assertThat(result.get().getProductId()).isEqualTo(product1.getId()); - assertThat(result.get().getIsSeller()).isTrue(); - assertThat(result.get().getBidAmount()).isEqualTo(0); - assertThat(result.get().getIsParticipated()).isFalse(); - assertThat(result.get().getBidId()).isNull(); - assertThat(result.get().getImages()) - .hasSize(2) - .extracting(ImageResponse::imageUrl) - .containsExactlyInAnyOrder(image1.getCdnPath(), image2.getCdnPath()); - assertThat(result.get().getIsCancelled()).isFalse(); - assertThat(result.get().getIsWinner()).isFalse(); - assertThat(result.get().getIsWon()).isFalse(); - assertThat(result.get().getIsOrdered()).isFalse(); - } - - @Test - @DisplayName("경매 상세 조회 - 다른 사람의 제품 경매 (참여하지 않은 경우)") - public void testFindAuctionDetailsById_OtherUser_NotParticipating() throws Exception { - //given - Long auctionId = auction1.getId(); - Long userId = user3.getId(); - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - - //then - assertThat(result).isPresent(); - assertThat(result.get().getProductId()).isEqualTo(product1.getId()); - assertThat(result.get().getIsSeller()).isFalse(); - assertThat(result.get().getBidAmount()).isEqualTo(0); - assertThat(result.get().getIsParticipated()).isFalse(); - assertThat(result.get().getBidId()).isNull(); - assertThat(result.get().getRemainingBidCount()).isEqualTo(3); - assertThat(result.get().getIsCancelled()).isFalse(); - assertThat(result.get().getIsWinner()).isFalse(); - assertThat(result.get().getIsWon()).isFalse(); - assertThat(result.get().getIsOrdered()).isFalse(); - assertThat(result.get().getIsWinner()).isFalse(); - assertThat(result.get().getIsWon()).isFalse(); - assertThat(result.get().getIsOrdered()).isFalse(); - } - - @Test - @DisplayName("경매 상세 조회 - 다른 사람의 제품 경매 (참여한 경우)") - public void testFindAuctionDetailsById_OtherUser_Participating() throws Exception { - //given - Long auctionId = auction2.getId(); - Long userId = user3.getId(); - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - - //then - assertThat(result).isPresent(); - AuctionDetailsResponse response = result.get(); - assertThat(response.getProductId()).isEqualTo(product2.getId()); - assertThat(response.getSellerNickname()).isEqualTo(user1.getNickname()); - assertThat(response.getProductName()).isEqualTo("제품2"); - assertThat(response.getMinPrice()).isEqualTo(20000); - assertThat(response.getStatus()).isEqualTo(PROCEEDING); - assertThat(response.getIsSeller()).isFalse(); - assertThat(response.getIsParticipated()).isTrue(); - assertThat(response.getBidAmount()).isEqualTo(6000L); // user3의 최신 입찰액 - assertThat(response.getBidId()).isNotNull(); - assertThat(response.getParticipantCount()).isGreaterThanOrEqualTo(2); // 최소 2명 (user2, user3) - assertThat(result.get().getImages()) - .hasSize(1) - .extracting(ImageResponse::imageUrl) - .containsExactlyInAnyOrder(image3.getCdnPath()); - assertThat(response.getIsCancelled()).isFalse(); - assertThat(result.get().getIsWinner()).isFalse(); - assertThat(result.get().getIsWon()).isFalse(); - assertThat(result.get().getIsOrdered()).isFalse(); - } - - @Test - @DisplayName("경매 상세 조회 - 없는 경매인 경우") - public void testFindAuctionDetailsById_NonExistentAuction() throws Exception { - //given - Long auctionId = 100L; - Long userId = user1.getId(); - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - - //then - assertThat(result).isNotPresent(); - } - - @Test - @DisplayName("경매 상세 조회 - 비로그인 상태에서 조회 할 경우") - public void testAuctionDetailsWhenUserIdIsNull() throws Exception { - //given - Long auctionId = auction2.getId(); - Long userId = null; - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - - //then - assertThat(result).isPresent(); - assertThat(result.get().getProductId()).isEqualTo(product2.getId()); - assertThat(result.get().getIsSeller()).isFalse(); - assertThat(result.get().getBidAmount()).isEqualTo(0L); - assertThat(result.get().getIsParticipated()).isFalse(); - assertThat(result.get().getBidId()).isNull(); - assertThat(result.get().getRemainingBidCount()).isEqualTo(3); - assertThat(result.get().getIsCancelled()).isFalse(); - assertThat(result.get().getIsWinner()).isFalse(); - assertThat(result.get().getIsWon()).isFalse(); - assertThat(result.get().getIsOrdered()).isFalse(); - } - - @Test - @DisplayName("경매 상세 조회 - 취소된 입찰이 있는 경우") - public void testFindAuctionDetailsById_WithCancelledBid() throws Exception { - //given - Long auctionId = auction9.getId(); - Long userId = user5.getId(); - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - - //then - assertThat(result).isPresent(); - AuctionDetailsResponse response = result.get(); - assertThat(response.getProductId()).isEqualTo(product9.getId()); - assertThat(response.getIsSeller()).isFalse(); - assertThat(response.getBidAmount()).isEqualTo(0L); - assertThat(response.getIsParticipated()).isFalse(); - assertThat(response.getBidId()).isNull(); - assertThat(response.getIsCancelled()).isTrue(); - assertThat(result.get().getIsWinner()).isFalse(); - assertThat(result.get().getIsWon()).isFalse(); - assertThat(result.get().getIsOrdered()).isFalse(); - } - - @Test - @DisplayName("주문이 있을 시 상세정보 조회를 한다") - public void shouldReturnAuctionDetailsWhenOrderExists() throws Exception { - //given - Long auctionId = auction8.getId(); - Long userId = user4.getId(); - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - AuctionDetailsResponse response = result.get(); - - //then - // 상품 ID 및 사용자 정보 검증 - assertThat(response.getProductId()).isEqualTo(product8.getId()); - assertThat(response.getIsSeller()).isFalse(); - assertThat(response.getIsWinner()).isTrue(); - assertThat(response.getIsOrdered()).isTrue(); - } - - @Test - @DisplayName("주문이 없을시 상세정보 조회를 한다") - public void shouldReturnAuctionDetailsWhenNoOrderExists() throws Exception { - //given - Long auctionId = auction9.getId(); - Long userId = null; - - //when - Optional result = auctionRepository.findAuctionDetailsById(auctionId, userId); - AuctionDetailsResponse response = result.get(); - - //then - // 상품 ID 및 사용자 정보 검증 - assertThat(response.getProductId()).isEqualTo(product9.getId()); - assertThat(response.getIsSeller()).isFalse(); - assertThat(response.getIsWinner()).isFalse(); - assertThat(response.getIsOrdered()).isFalse(); - } - - @Test - @DisplayName("특정 유저의 경매 목록 조회 - 최신순") - public void testFindMyAuctionsWithNewest() throws Exception { - //given - Pageable pageable = PageRequest.of(0, 10, Sort.by("newest")); - - //when - Page result = auctionRepository.findAuctionsByNickname( - user1.getNickname(), pageable); - - //then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent().get(0).getCreatedAt()).isAfter(result.getContent().get(1).getCreatedAt()); - } - - @Test - @DisplayName("특정 유저의 경매 목록 조회 - 오래된순") - public void testFindMyAuctionsWithOldest() throws Exception { - //given - Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("newest"))); - - //when - Page result = auctionRepository.findAuctionsByNickname( - user1.getNickname(), pageable); - - //then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent().get(0).getCreatedAt()).isBefore(result.getContent().get(1).getCreatedAt()); - } - - @Test - @DisplayName("나의 경매 목록 조회했는데 없는 경우") - public void testFindMyAuctionsNotExist() throws Exception { - //given - Pageable pageable = PageRequest.of(0, 10, Sort.by("newest")); - - //when - Page result = auctionRepository.findAuctionsByNickname( - user4.getNickname(), pageable); - - //then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(0); - } - - @Test - @DisplayName("베스트 경매 조회") - void testFindBestAuctions() { - // given - List bestAuctions = auctionRepository.findBestAuctions(); - // when - - // then - assertThat(bestAuctions).isSortedAccordingTo( - Comparator.comparingLong(AuctionResponse::getParticipantCount).reversed()); - } - - @Test - @DisplayName("마감 임박 경매 조회") - void testImminentAuctions() { - // given - List imminentAuctions = auctionRepository.findImminentAuctions(); - // then - assertThat(imminentAuctions).isNotEmpty(); - assertThat(imminentAuctions.size()).isEqualTo(3); - assertThat(imminentAuctions).allMatch(auctionResponse -> auctionResponse.getTimeRemaining() <= 3600); - assertThat(imminentAuctions).isSortedAccordingTo( - Comparator.comparing(BaseAuctionDto::getTimeRemaining)); - - } - - @Test - @DisplayName("사용자의 실패한 경매 조회") - void testGetLostAuctionHistory() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by(Direction.DESC, "endDateTime")); - - // when - Page result = auctionRepository.findLostAuctionHistoryByUserId(user2.getId(), pageable); - - // then - assertNotNull(result); - result.getContent().forEach(System.out::println); - assertEquals(2, result.getTotalElements()); // user2는 2개의 경매에서 낙찰하지 못했음 - - // 첫 번째 실패한 경매 - LostAuctionResponse firstLost = result.getContent().get(0); - assertThat(firstLost.auctionId()).isEqualTo(auction4.getId()); - assertThat(firstLost.productName()).isEqualTo("제품4"); - assertThat(firstLost.imageUrl()).isEqualTo("path/to/image4.jpg"); - assertThat(firstLost.minPrice()).isEqualTo(40000); - assertThat(firstLost.bidAmount()).isEqualTo(15000L); - - // 두 번째 실패한 경매 - LostAuctionResponse secondLost = result.getContent().get(1); - assertThat(secondLost.auctionId()).isEqualTo(auction8.getId()); - assertThat(secondLost.productName()).isEqualTo("제품8"); - assertThat(secondLost.imageUrl()).isEqualTo("path/to/image5.jpg"); - assertThat(secondLost.minPrice()).isEqualTo(75000); - assertThat(secondLost.bidAmount()).isEqualTo(150000L); - - // 정렬 순서 확인 (종료 시간 기준 내림차순) - assertThat(result.getContent()).isSortedAccordingTo( - Comparator.comparing(LostAuctionResponse::endDateTime).reversed() - ); - - } - - @Nested - @DisplayName("사용자 정보 조회 테스트") - class getUserProfileTest { - @Autowired - private UserRepository userRepository; - - @Autowired - private ProductRepository productRepository; - - @Autowired - private AuctionRepository auctionRepository; - - @Autowired - private BidRepository bidRepository; - - User user, seller; - Product successedProduct, ongoingProduct1, ongoingProduct2, failedProduct1, failedProduct2; - - @BeforeEach - @Transactional - void setUp() { - user = User.builder() - .email("test01@gmail.com") - .providerId("132456798") - .build(); - seller = User.builder() - .email("test02@gmail.com") - .providerId("222222222") - .build(); - userRepository.saveAll(List.of(user, seller)); - - successedProduct = Product.builder() - .user(seller) - .category(Category.BOOKS_AND_MEDIA) - .name("product1") - .minPrice(10000) - .build(); - ongoingProduct1 = Product.builder() - .user(seller) - .category(Category.OTHER) - .name("product2") - .minPrice(20000) - .build(); - - ongoingProduct2 = Product.builder() - .user(seller) - .category(Category.OTHER) - .name("product3") - .minPrice(20000) - .build(); - - failedProduct1 = Product.builder() - .user(seller) - .category(Category.OTHER) - .name("product4") - .minPrice(20000) - .build(); - - failedProduct2 = Product.builder() - .user(seller) - .category(Category.OTHER) - .name("product5") - .minPrice(20000) - .build(); - - productRepository.saveAll( - List.of(ongoingProduct1, ongoingProduct2, failedProduct1, failedProduct2, successedProduct)); - - Auction ongoingAuction1 = Auction.builder() - .endDateTime(LocalDateTime.now().plusHours(1)) - .product(ongoingProduct1) - .status(PROCEEDING) - .build(); - - Auction ongoingAuction2 = Auction.builder() - .endDateTime(LocalDateTime.now().plusHours(1)) - .product(ongoingProduct2) - .status(PROCEEDING) - .build(); - - Auction failedAuction1 = Auction.builder() - .endDateTime(LocalDateTime.now().minusHours(1)) - .product(failedProduct1) - .status(ENDED) - .winnerId(2L) - .build(); - - Auction failedAuction2 = Auction.builder() - .endDateTime(LocalDateTime.now().minusHours(1)) - .product(failedProduct2) - .status(ENDED) - .winnerId(2L) - .build(); - - Auction successedAuction = Auction.builder() - .endDateTime(LocalDateTime.now().minusHours(1)) - .product(successedProduct) - .status(ENDED) - .winnerId(user.getId()) - .build(); - auctionRepository.saveAll( - List.of(ongoingAuction1, ongoingAuction2, failedAuction1, failedAuction2, successedAuction)); - - Bid bid1 = Bid.builder() - .bidderId(user.getId()) - .auctionId(successedAuction.getId()) - .amount(10000L) - .build(); - Bid bid2 = Bid.builder() - .bidderId(user.getId()) - .auctionId(failedAuction1.getId()) - .amount(10000L) - .build(); - Bid bid3 = Bid.builder() - .bidderId(user.getId()) - .auctionId(failedAuction2.getId()) - .amount(10000L) - .build(); - Bid bid4 = Bid.builder() - .bidderId(user.getId()) - .auctionId(ongoingAuction1.getId()) - .amount(1000L) - .build(); - Bid bid5 = Bid.builder() - .bidderId(user.getId()) - .auctionId(ongoingAuction2.getId()) - .amount(1000L) - .build(); - - bidRepository.saveAll(List.of(bid1, bid2, bid3, bid4, bid5)); - } - - @Test - @DisplayName("경매 수 정상 조회") - public void successfulCount() { - ParticipationCountsResponse counts = auctionRepository.getParticipationCounts(user.getId()); - assertThat(counts).isNotNull(); - assertThat(counts.ongoingAuctionCount()).isEqualTo(2); - assertThat(counts.successfulAuctionCount()).isEqualTo(1); - assertThat(counts.failedAuctionCount()).isEqualTo(2); - } - } - - @Test - @DisplayName("사용자의 진행 중인 경매 목록 조회") - void testFindProceedingAuctionByUserId() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - Page result = auctionRepository.findProceedingAuctionByUserId(user1.getId(), pageable); - - // then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(2); // user1이 진행 중인 경매는 2개 - assertThat(result.getContent().get(0).getStatus()).isEqualTo(PROCEEDING); - assertThat(result.getContent().get(0).getProductName()).isIn("제품1", "제품2"); - assertThat(result.getContent().get(1).getStatus()).isEqualTo(PROCEEDING); - } - - @Test - @DisplayName("사용자의 종료된 경매 목록 조회") - void testFindEndedAuctionByUserId() { - // given - Long userId = user2.getId(); // user2이 판매자로 등록한 경매를 조회 - Pageable pageable = PageRequest.of(0, 10); - - // when - Page result = auctionRepository.findEndedAuctionByUserId(userId, pageable); - - // then - assertNotNull(result); - assertThat(result.getContent()).isNotEmpty(); - assertThat(result.getContent()).hasSize(3); // user2의 종료된 경매는 2개 - } - - @Test - @DisplayName("사용자의 종료된 경매 목록 조회 - 다양한 상황 처리") - void testFindEndedAuctionByUserId_MultipleScenarios() { - // given - Long userId = user2.getId(); // user2의 종료된 경매 조회 - Pageable pageable = PageRequest.of(0, 10); - - // when - Page result = auctionRepository.findEndedAuctionByUserId(userId, pageable); - - // then - assertNotNull(result); - List content = result.getContent(); - - // 총 3개의 종료된 경매가 있어야 함 (auction4, auction8, auction9) - assertThat(content).hasSize(3); - - // 각각의 경매를 확인하기 위해 맵으로 변환 - Map auctionResponseMap = content.stream() - .collect(Collectors.toMap(UserEndedAuctionResponse::auctionId, Function.identity())); - - // auction4 검증 (결제 전 낙찰자 있음) - UserEndedAuctionResponse auction4Response = auctionResponseMap.get(auction4.getId()); - assertNotNull(auction4Response); - assertThat(auction4Response.productName()).isEqualTo("제품4"); - assertThat(auction4Response.winningBidAmount()).isEqualTo(25000L); // 최고 입찰가 - assertThat(auction4Response.isWon()).isTrue(); - assertThat(auction4Response.isOrdered()).isFalse(); // 결제 전 - assertThat(auction4Response.participantCount()).isEqualTo(2); // bid11, bid12 - - // auction8 검증 (결제 후 낙찰자 있음) - UserEndedAuctionResponse auction8Response = auctionResponseMap.get(auction8.getId()); - assertNotNull(auction8Response); - assertThat(auction8Response.productName()).isEqualTo("제품8"); - assertThat(auction8Response.winningBidAmount()).isEqualTo(250000L); // 최고 입찰가 - assertThat(auction8Response.isWon()).isTrue(); - assertThat(auction8Response.isOrdered()).isTrue(); // 결제 완료 - assertThat(auction8Response.participantCount()).isEqualTo(2); // bid13, bid14 - - // auction10 검증 (낙찰자 없음) - UserEndedAuctionResponse auction9Response = auctionResponseMap.get(auction10.getId()); - assertNotNull(auction9Response); - assertThat(auction9Response.productName()).isEqualTo("제품10"); - assertThat(auction9Response.winningBidAmount()).isEqualTo(0L); // 입찰 없음 - assertThat(auction9Response.isWon()).isFalse(); - assertThat(auction9Response.isOrdered()).isFalse(); // 결제 없음 - assertThat(auction9Response.participantCount()).isEqualTo(0); // 입찰 없음 - } - - @Test - @DisplayName("낙찰정보 조회에 성공한다.") - public void findWinningBidById_Success() { - //given - Long auctionId = auction8.getId(); - - //when - Optional result = auctionRepository.findWinningBidById(auctionId); - WonAuctionDetailsResponse response = result.get(); - //then - assertThat(response.auctionId()).isEqualTo(auction8.getId()); - assertThat(response.productName()).isEqualTo(product8.getName()); - assertThat(response.winningAmount()).isEqualTo(250000L); - } - - @Test - @DisplayName("낙찰정보가 없는 경매조회시 빈 Optional를 반환한다.") - public void findWinningBidById_EmptyOptional_WhenNoWinningBid() { - //given - Long auctionId = auction9.getId(); - - //when - Optional result = auctionRepository.findWinningBidById(auctionId); - assertThat(result).isEmpty(); - } - -} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java b/src/test/java/org/chzz/market/domain/auction/service/AuctionDeleteServiceTest.java similarity index 79% rename from src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java rename to src/test/java/org/chzz/market/domain/auction/service/AuctionDeleteServiceTest.java index 73aa8064..ddd7c86b 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionDeleteServiceTest.java +++ b/src/test/java/org/chzz/market/domain/auction/service/AuctionDeleteServiceTest.java @@ -1,9 +1,9 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.OFFICIAL_AUCTION_DELETE_FORBIDDEN; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ACCESS_FORBIDDEN; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.OFFICIAL_AUCTION_DELETE_FORBIDDEN; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; @@ -14,12 +14,12 @@ import java.util.List; import java.util.Optional; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.imagev2.service.ImageDeleteService; -import org.chzz.market.domain.likev2.entity.LikeV2; -import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.image.service.ImageDeleteService; +import org.chzz.market.domain.like.entity.Like; +import org.chzz.market.domain.like.repository.LikeRepository; import org.chzz.market.domain.notification.event.NotificationEvent; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,9 +35,9 @@ class AuctionDeleteServiceTest { @Mock private ImageDeleteService imageDeleteService; @Mock - private AuctionV2Repository auctionRepository; + private AuctionRepository auctionRepository; @Mock - private LikeV2Repository likeRepository; + private LikeRepository likeRepository; @Mock private ApplicationEventPublisher eventPublisher; @@ -47,8 +47,8 @@ class AuctionDeleteServiceTest { @Test void 정상_삭제_테스트() { // given - AuctionV2 auction = mock(AuctionV2.class); - LikeV2 like = mock(LikeV2.class); + Auction auction = mock(Auction.class); + Like like = mock(Like.class); when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); doNothing().when(auction).validateOwner(any()); @@ -67,7 +67,7 @@ class AuctionDeleteServiceTest { @Test void 정상_삭제_테스트_좋아요_누른사람이_없을때_알림이벤트발행_하지않는다() { // given - AuctionV2 auction = mock(AuctionV2.class); + Auction auction = mock(Auction.class); when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); doNothing().when(auction).validateOwner(any()); @@ -98,7 +98,7 @@ class AuctionDeleteServiceTest { @Test void 해당_경매에_주인이_아닐때_삭제시도_할경우_예외가_발생한다() { // given - AuctionV2 auction = mock(AuctionV2.class); + Auction auction = mock(Auction.class); // when when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); @@ -114,7 +114,7 @@ class AuctionDeleteServiceTest { @Test void 사전경매가_아닐때_삭제시도_할경우_예외가_발생한다() { // given - AuctionV2 auction = mock(AuctionV2.class); + Auction auction = mock(Auction.class); // when when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java b/src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java similarity index 70% rename from src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java rename to src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java index 5aa1e8b3..239163c4 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionLookupServiceTest.java +++ b/src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java @@ -1,11 +1,11 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.error.AuctionException; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.error.AuctionException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; diff --git a/src/test/java/org/chzz/market/domain/auction/service/AuctionServiceTest.java b/src/test/java/org/chzz/market/domain/auction/service/AuctionServiceTest.java deleted file mode 100644 index b32516e9..00000000 --- a/src/test/java/org/chzz/market/domain/auction/service/AuctionServiceTest.java +++ /dev/null @@ -1,850 +0,0 @@ -package org.chzz.market.domain.auction.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ALREADY_REGISTERED; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_NOT_FOUND; -import static org.chzz.market.domain.auction.type.AuctionRegisterType.PRE_REGISTER; -import static org.chzz.market.domain.auction.type.AuctionRegisterType.REGISTER; -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.product.entity.Product.Category.ELECTRONICS; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.anyList; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.dto.request.PreRegisterRequest; -import org.chzz.market.domain.auction.dto.request.RegisterAuctionRequest; -import org.chzz.market.domain.auction.dto.request.StartAuctionRequest; -import org.chzz.market.domain.auction.dto.response.AuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; -import org.chzz.market.domain.auction.dto.response.RegisterResponse; -import org.chzz.market.domain.auction.dto.response.SimpleAuctionResponse; -import org.chzz.market.domain.auction.dto.response.StartAuctionResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionDetailsResponse; -import org.chzz.market.domain.auction.dto.response.WonAuctionResponse; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.error.AuctionException; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.auction.service.register.AuctionRegisterService; -import org.chzz.market.domain.auction.service.register.PreRegisterService; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.error.ProductErrorCode; -import org.chzz.market.domain.product.error.ProductException; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.error.exception.UserException; -import org.chzz.market.domain.user.repository.UserRepository; -import org.chzz.market.util.AuctionTestFactory; -import org.chzz.market.util.ProductTestFactory; -import org.chzz.market.util.UserTestFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -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.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.multipart.MultipartFile; - -@ExtendWith(MockitoExtension.class) -class AuctionServiceTest { - @Mock - private AuctionRepository auctionRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private ProductRepository productRepository; - - @Mock - private ImageService imageService; - - @InjectMocks - private AuctionService auctionService; - @InjectMocks - private PreRegisterService preRegisterService; - @InjectMocks - private AuctionRegisterService auctionRegisterService; - - private ProductTestFactory productTestFactory; - private AuctionTestFactory auctionTestFactory; - private UserTestFactory userTestFactory; - - private User user; - private BaseRegisterRequest registerAuctionRequest, preRegisterRequest; - private StartAuctionRequest validStartAuctionRequest, invalidStartAuctionRequest; - - @BeforeEach - void setUp() { - productTestFactory = new ProductTestFactory(); - auctionTestFactory = new AuctionTestFactory(); - userTestFactory = new UserTestFactory(); - - user = User.builder() - .id(1L) - .email("test@naver.com") - .nickname("테스트 유저") - .build(); - - registerAuctionRequest = RegisterAuctionRequest.builder() - .productName("경매 등록 테스트 상품 이름") - .description("경매 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(REGISTER) - .build(); - - preRegisterRequest = PreRegisterRequest.builder() - .productName("사전 등록 테스트 상품 이름") - .description("사전 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(PRE_REGISTER) - .build(); - - validStartAuctionRequest = StartAuctionRequest.builder() - .productId(1L) - .build(); - - invalidStartAuctionRequest = StartAuctionRequest.builder() - .productId(999L) - .build(); - - System.setProperty("org.mockito.logging.verbosity", "all"); - } - - @Nested - @DisplayName("상품 사전 등록 테스트") - class PreRegisterTest { - - @Test - @DisplayName("1. 유효한 요청으로 상품 사전 등록 성공 응답") - void preRegister_Success() { - // given - Long userId = 1L; - Long productId = 1L; - - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - when(imageService.uploadImages(anyList())).thenReturn(List.of("image1.jpg", "image2.jpg")); - - List images = createMockMultipartFiles(); - - Product savedProduct = ProductTestFactory.createProduct(preRegisterRequest, user); - savedProduct.addImages(createExistingImages(savedProduct)); - ReflectionTestUtils.setField(savedProduct, "id", productId); - - when(productRepository.save(any(Product.class))).thenReturn(savedProduct); - - // when - RegisterResponse response = preRegisterService.register(userId, preRegisterRequest, images); - - // then - assertNotNull(response); - assertEquals(productId, response.getProductId()); - verify(userRepository, times(1)).findById(userId); - verify(productRepository, times(1)).save(any(Product.class)); - verify(imageService, times(1)).uploadImages(anyList()); - } - - @Test - @DisplayName("2. 존재하지 않는 사용자로 상품 사전 등록 실패") - void preRegister_UserNotFound() { - // Given - Long userId = 999L; - PreRegisterRequest invalidPreRegisterRequest = PreRegisterRequest.builder() - .productName("사전 등록 테스트 상품 이름") - .description("사전 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(PRE_REGISTER) - .build(); - - when(userRepository.findById(userId)).thenReturn(Optional.empty()); - - List images = createMockMultipartFiles(); - - // When & Then - assertThrows(UserException.class, () -> { - preRegisterService.register(userId, invalidPreRegisterRequest, images); - }); - - // verify - verify(productRepository, never()).save(any(Product.class)); - verify(auctionRepository, never()).save(any(Auction.class)); - verify(imageService, never()).uploadImages(anyList()); - - } - } - - @Nested - @DisplayName("상품 경매 등록 테스트") - class RegisterAuctionTest { - - @Test - @DisplayName("1. 유효한 요청으로 경매 상품 등록 성공 응답") - void registerAuction_Success() { - // given - Long userId = 1L; - Long productId = 1L; - Long auctionId = 1L; - - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(imageService.uploadImages(anyList())).thenReturn(List.of("image1.jpg", "image2.jpg")); - - List images = createMockMultipartFiles(); - - Product product = ProductTestFactory.createProduct(registerAuctionRequest, user); - product.addImages(createExistingImages(product)); - ReflectionTestUtils.setField(product, "id", productId); - - Auction auction = AuctionTestFactory.createAuction(product, registerAuctionRequest, PROCEEDING); - ReflectionTestUtils.setField(auction, "id", auctionId); - - when(productRepository.save(any(Product.class))).thenReturn(product); - when(auctionRepository.save(any(Auction.class))).thenReturn(auction); - - // when - RegisterResponse response = auctionRegisterService.register(userId, registerAuctionRequest, images); - - // then - assertNotNull(response); - assertEquals(productId, response.getProductId()); - verify(userRepository, times(1)).findById(userId); - verify(productRepository, times(1)).save(any(Product.class)); - verify(auctionRepository, times(1)).save(any(Auction.class)); - verify(imageService, times(1)).uploadImages(anyList()); - } - - @Test - @DisplayName("2. 존재하지 않는 사용자로 상품 경매 등록 실패") - void registerAuction_UserNotFound() { - // Given - Long userId = 999L; - RegisterAuctionRequest invalidRegisterAuctionRequest = RegisterAuctionRequest.builder() - .productName("경매 등록 테스트 상품 이름") - .description("경매 등록 테스트 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .auctionRegisterType(REGISTER) - .build(); - - when(userRepository.findById(999L)).thenReturn(Optional.empty()); - - List images = createMockMultipartFiles(); - - // When & Then - assertThrows(UserException.class, () -> { - auctionRegisterService.register(userId, invalidRegisterAuctionRequest, images); - }); - - // verify - verify(productRepository, never()).save(any(Product.class)); - verify(auctionRepository, never()).save(any(Auction.class)); - verify(imageService, never()).uploadImages(anyList()); - - } - } - - @Nested - @DisplayName("사전 등록 된 상품 경매 등록 상품으로 전환 테스트") - class StartAuctionTest { - - @Test - @DisplayName("1. 유효한 요청으로 사전 등록 된 상품 경매 등록 전환 성공 응답") - void startAuction_Success() { - // given - Long productId = 1L; - Long newAuctionId = 2L; - LocalDateTime now = LocalDateTime.now(); - - Product preRegisteredProduct = ProductTestFactory.createProduct(preRegisterRequest, user); - ReflectionTestUtils.setField(preRegisteredProduct, "id", productId); - - Auction newAuction = AuctionTestFactory.createAuction(preRegisteredProduct, registerAuctionRequest, - PROCEEDING); - ReflectionTestUtils.setField(newAuction, "id", newAuctionId); - ReflectionTestUtils.setField(newAuction, "endDateTime", LocalDateTime.now().plusHours(24)); - - when(productRepository.findById(productId)).thenReturn(Optional.of(preRegisteredProduct)); - when(auctionRepository.existsByProductId(productId)).thenReturn(false); - when(auctionRepository.save(any(Auction.class))).thenReturn(newAuction); - - // when - StartAuctionResponse response = auctionService.startAuction(1L, validStartAuctionRequest); - - // then - assertNotNull(response); - assertEquals(newAuctionId, response.auctionId()); - assertEquals(productId, response.productId()); - assertEquals(PROCEEDING, response.status()); - assertTrue(response.endDateTime().isAfter(now) && response.endDateTime().isBefore(now.plusHours(25))); - - verify(productRepository).findById(productId); - verify(auctionRepository).existsByProductId(productId); - verify(auctionRepository).save(any(Auction.class)); - } - - @Test - @DisplayName("2. 존재하지 않는 상품 ID로 전환 시도 실패") - void startAuction_NotFound() { - // Given - Long nonExistentProductId = 999L; - when(productRepository.findById(nonExistentProductId)).thenReturn(Optional.empty()); - - // When & Then - ProductException exception = assertThrows(ProductException.class, - () -> auctionService.startAuction(any(), invalidStartAuctionRequest)); - - assertEquals(ProductErrorCode.PRODUCT_NOT_FOUND, exception.getErrorCode()); - verify(auctionRepository, never()).save(any(Auction.class)); - } - - @Test - @DisplayName("3. 이미 등록된 경매 상품 전환 시도 실패") - void startAuction_AlreadyProceeding() { - // given - Long productId = 1L; - - Product product = ProductTestFactory.createProduct(preRegisterRequest, user); - ReflectionTestUtils.setField(product, "id", productId); - - when(productRepository.findById(productId)).thenReturn(Optional.of(product)); - when(auctionRepository.existsByProductId(productId)).thenReturn(true); - - // When & Then - AuctionException exception = assertThrows(AuctionException.class, - () -> auctionService.startAuction(1L, validStartAuctionRequest)); - assertEquals(AUCTION_ALREADY_REGISTERED, exception.getErrorCode()); - - verify(auctionRepository, never()).save(any(Auction.class)); - } - } - - @Nested - @DisplayName("경매 상세 조회 테스트") - class GetAuctionDetailsTest { - @Test - @DisplayName("1. 값이 채워진 경우 예외 발생 안함") - public void testGetAuctionDetails_ExistingAuction_NoException() { - // given - Long existingAuctionId = 1L; - Long userId = 1L; - AuctionDetailsResponse auctionDetails = new AuctionDetailsResponse(1L, "닉네임2", "null", "제품1", null, 1000, - ELECTRONICS, 123L, PROCEEDING, false, 0L, false, null, 0L, 0, false, false, false, null); - - // when - when(auctionRepository.findAuctionDetailsById(anyLong(), anyLong())).thenReturn( - Optional.of(auctionDetails)); - - // then - assertDoesNotThrow(() -> { - auctionService.getFullAuctionDetails(existingAuctionId, userId); - }); - } - - @Test - @DisplayName("2. 빈 값이 리턴 되는 경우 예외 발생") - public void testGetAuctionDetails_NonExistentAuction() { - // given - Long nonExistentAuctionId = 999L; - Long userId = 1L; - - // when - when(auctionRepository.findAuctionDetailsById(anyLong(), anyLong())).thenReturn(Optional.empty()); - - // then - AuctionException auctionException = assertThrows(AuctionException.class, () -> { - auctionService.getFullAuctionDetails(nonExistentAuctionId, userId); - }); - assertThat(auctionException.getErrorCode()).isEqualTo(AUCTION_NOT_FOUND); - } - - @Test - @DisplayName("3. 낙찰자인 경우 isOrdered 값이 유지됨") - void shouldMaintainIsOrderedWhenUserIsWinner() { - // given - Long auctionId = 1L; - Long userId = 1L; - AuctionDetailsResponse auctionDetails = new AuctionDetailsResponse( - 1L, "닉네임1", "profile.jpg", "제품1", "설명", 1000, - ELECTRONICS, 100L, PROCEEDING, false, 5L, true, 10L, 1000L, 3, false, true, true, true); - - // 판매자이거나 낙찰자인 경우 - when(auctionRepository.findAuctionDetailsById(anyLong(), anyLong())).thenReturn(Optional.of(auctionDetails)); - - // when - AuctionDetailsResponse result = auctionService.getFullAuctionDetails(auctionId, userId); - - // then - assertNotNull(result); - assertThat(result.getIsOrdered()).isTrue(); // isOrdered 값이 null로 초기화되지 않음 - } - - @Test - @DisplayName("4. 판매자인 경우 isOrdered 값이 유지됨") - void shouldMaintainIsOrderedWhenUserIsSeller() { - // given - Long auctionId = 1L; - Long userId = 1L; - AuctionDetailsResponse auctionDetails = new AuctionDetailsResponse( - 1L, "닉네임1", "profile.jpg", "제품1", "설명", 1000, - ELECTRONICS, 100L, PROCEEDING, true, 5L, false, null, 0L, 3, false, false, true, false); - - // 판매자이거나 낙찰자인 경우 - when(auctionRepository.findAuctionDetailsById(anyLong(), anyLong())).thenReturn(Optional.of(auctionDetails)); - - // when - AuctionDetailsResponse result = auctionService.getFullAuctionDetails(auctionId, userId); - - // then - assertNotNull(result); - assertThat(result.getIsOrdered()).isFalse(); // isOrdered 값이 null로 초기화되지 않음 - } - - @Test - @DisplayName("5. 판매자도 낙찰자도 아닌 경우 isOrdered 값이 null로 초기화됨") - void shouldSetIsOrderedToNullWhenNotSellerOrWinner() { - // given - Long auctionId = 1L; - Long userId = 2L; // 판매자나 낙찰자가 아닌 사용자 - AuctionDetailsResponse auctionDetails = new AuctionDetailsResponse( - 1L, "닉네임1", "profile.jpg", "제품1", "설명", 1000, - ELECTRONICS, 100L, PROCEEDING, false, 5L, false, null, 0L, 3, false, false, false, true); - - // 판매자나 낙찰자가 아닌 경우 - when(auctionRepository.findAuctionDetailsById(anyLong(), anyLong())).thenReturn(Optional.of(auctionDetails)); - - // when - AuctionDetailsResponse result = auctionService.getFullAuctionDetails(auctionId, userId); - - // then - assertNotNull(result); - assertThat(result.getIsOrdered()).isNull(); // isOrdered 값이 null로 초기화됨 - } - - @Test - @DisplayName("6. 비회원일 경우 isOrdered 값이 null로 초기화됨") - void shouldSetIsOrderedToNullForNonMember() { - // given - Long auctionId = 1L; - Long userId = null; // 비회원 - AuctionDetailsResponse auctionDetails = new AuctionDetailsResponse( - 1L, "닉네임1", "profile.jpg", "제품1", "설명", 1000, - ELECTRONICS, 100L, PROCEEDING, false, 5L, false, null, 0L, 3, false, false, false, true); - - // 판매자나 낙찰자가 아닌 경우 - when(auctionRepository.findAuctionDetailsById(auctionId, userId)).thenReturn(Optional.of(auctionDetails)); - - // when - AuctionDetailsResponse result = auctionService.getFullAuctionDetails(auctionId, userId); - - // then - assertNotNull(result); - assertThat(result.getIsOrdered()).isNull(); // isOrdered 값이 null로 초기화됨 - } - } - - @Nested - @DisplayName("경매 간단 상세 조회 테스트") - class GetSimpleAuctionDetailsTest { - @Test - @DisplayName("1. 판매자가 자신의 경매 상품을 조회할 때 성공") - void getSimpleAuctionDetails_Success() { - // given - Long auctionId = 1L; - SimpleAuctionResponse response = new SimpleAuctionResponse("image1.jpg", "Product 1", 10000, 5L); - - when(auctionRepository.findSimpleAuctionDetailsById(auctionId)).thenReturn(Optional.of(response)); - - // when - SimpleAuctionResponse result = auctionService.getSimpleAuctionDetails(auctionId); - - // then - assertNotNull(result); - assertEquals("image1.jpg", result.imageUrl()); - assertEquals("Product 1", result.productName()); - assertEquals(10000, result.minPrice()); - assertEquals(5L, result.participantCount()); - - verify(auctionRepository).findSimpleAuctionDetailsById(auctionId); - } - - @Test - @DisplayName("2. 판매자가 아닌 사용자가 경매 상품을 조회할 때 예외 발생") - void getSimpleAuctionDetails_NotAccessible() { - // given - Long auctionId = 1L; - Auction auction = mock(Auction.class); - Product product = mock(Product.class); - - // when & then - AuctionException exception = assertThrows(AuctionException.class, - () -> auctionService.getSimpleAuctionDetails(auctionId)); - assertEquals(AUCTION_NOT_FOUND, exception.getErrorCode()); - } - - @Test - @DisplayName("3. 존재하지 않는 경매 상품을 조회할 때 예외 발생") - void getSimpleAuctionDetails_NotFound() { - // given - Long nonExistentAuctionId = 999L; - - // when & then - AuctionException exception = assertThrows(AuctionException.class, - () -> auctionService.getSimpleAuctionDetails(nonExistentAuctionId)); - assertEquals(AUCTION_NOT_FOUND, exception.getErrorCode()); - } - } - - @Nested - @DisplayName("내가 성공한 경매 조회 테스트") - class GetWonAuctionHistoryTest { - @Test - @DisplayName("1. 유효한 요청으로 낙찰된 경매 조회 성공") - void getWonAuctionHistory_Success() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - - List wonAuctions = List.of( - new WonAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, LocalDateTime.now(), 15000L,false,null), - new WonAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, LocalDateTime.now(), 25000L,false,null) - ); - - Page mockPage = new PageImpl<>(wonAuctions, pageable, wonAuctions.size()); - - when(auctionRepository.findWonAuctionHistoryByUserId(userId, pageable)).thenReturn(mockPage); - - // when - Page resultPage = auctionService.getWonAuctionHistory(userId, pageable); - - // then - assertThat(resultPage).isNotNull(); - assertThat(resultPage.getContent()).hasSize(2); - assertThat(resultPage.getContent().get(0).auctionId()).isEqualTo(1L); - assertThat(resultPage.getContent().get(0).productName()).isEqualTo("Product 1"); - assertThat(resultPage.getContent().get(1).auctionId()).isEqualTo(2L); - assertThat(resultPage.getContent().get(1).productName()).isEqualTo("Product 2"); - - verify(auctionRepository, times(1)).findWonAuctionHistoryByUserId(userId, pageable); - } - - @Test - @DisplayName("2. 낙찰된 경매가 없는 경우 빈 목록 반환") - void getWonAuctionHistory_EmptyList() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0); - - when(auctionRepository.findWonAuctionHistoryByUserId(userId, pageable)).thenReturn(emptyPage); - - // when - Page resultPage = auctionService.getWonAuctionHistory(userId, pageable); - - // then - assertThat(resultPage).isNotNull(); - assertThat(resultPage.getContent()).isEmpty(); - assertThat(resultPage.getTotalElements()).isZero(); - - verify(auctionRepository, times(1)).findWonAuctionHistoryByUserId(userId, pageable); - } - - @Test - @DisplayName("3. 페이지네이션 동작 확인") - void getWonAuctionHistory_Pagination() { - // given - Long userId = 1L; - Pageable firstPageable = PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "endDateTime")); - Pageable secondPageable = PageRequest.of(1, 1, Sort.by(Sort.Direction.DESC, "endDateTime")); - - List allAuctions = List.of( - new WonAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, LocalDateTime.now(), 15000L,false,null), - new WonAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, LocalDateTime.now(), 25000L,false,null) - ); - - Page firstPage = new PageImpl<>(allAuctions.subList(0, 1), firstPageable, - allAuctions.size()); - Page secondPage = new PageImpl<>(allAuctions.subList(1, 2), secondPageable, - allAuctions.size()); - - when(auctionRepository.findWonAuctionHistoryByUserId(userId, firstPageable)).thenReturn(firstPage); - when(auctionRepository.findWonAuctionHistoryByUserId(userId, secondPageable)).thenReturn(secondPage); - - // when - Page firstResultPage = auctionService.getWonAuctionHistory(userId, firstPageable); - Page secondResultPage = auctionService.getWonAuctionHistory(userId, secondPageable); - - // then - assertThat(firstResultPage.getContent()).hasSize(1); - assertThat(firstResultPage.getContent().get(0).auctionId()).isEqualTo(1L); - assertThat(secondResultPage.getContent()).hasSize(1); - assertThat(secondResultPage.getContent().get(0).auctionId()).isEqualTo(2L); - - verify(auctionRepository, times(1)).findWonAuctionHistoryByUserId(userId, firstPageable); - verify(auctionRepository, times(1)).findWonAuctionHistoryByUserId(userId, secondPageable); - } - - @Test - @DisplayName("4. 정렬 순서 확인 (경매 종료 시간 내림차순)") - void getWonAuctionHistory_SortOrder() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - - LocalDateTime now = LocalDateTime.now(); - List wonAuctions = List.of( - new WonAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, now, 15000L,false,null), - new WonAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, now.minusHours(1), 25000L,false,null), - new WonAuctionResponse(3L, "Product 3", "image3.jpg", 30000, 3L, now.minusHours(2), 35000L,false,null) - ); - - Page mockPage = new PageImpl<>(wonAuctions, pageable, wonAuctions.size()); - - when(auctionRepository.findWonAuctionHistoryByUserId(userId, pageable)).thenReturn(mockPage); - - // when - Page resultPage = auctionService.getWonAuctionHistory(userId, pageable); - - // then - assertThat(resultPage.getContent()).hasSize(3); - assertThat(resultPage.getContent()).isSortedAccordingTo( - Comparator.comparing(WonAuctionResponse::endDateTime).reversed() - ); - - verify(auctionRepository, times(1)).findWonAuctionHistoryByUserId(userId, pageable); - } - } - - @Nested - @DisplayName("내가 실패한 경매 조회 테스트") - class GetLostAuctionHistoryTest { - @Test - @DisplayName("1. 유효한 요청으로 낙찰하지 못한 경매 조회 성공") - void getLostAuctionHistory_Success() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - - List LostAuctions = List.of( - new LostAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, LocalDateTime.now(), 15000L), - new LostAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, LocalDateTime.now(), 25000L) - ); - - Page mockPage = new PageImpl<>(LostAuctions, pageable, LostAuctions.size()); - - when(auctionRepository.findLostAuctionHistoryByUserId(userId, pageable)).thenReturn(mockPage); - - // when - Page resultPage = auctionService.getLostAuctionHistory(userId, pageable); - - // then - assertThat(resultPage).isNotNull(); - assertThat(resultPage.getContent()).hasSize(2); - assertThat(resultPage.getContent().get(0).auctionId()).isEqualTo(1L); - assertThat(resultPage.getContent().get(0).productName()).isEqualTo("Product 1"); - assertThat(resultPage.getContent().get(1).auctionId()).isEqualTo(2L); - assertThat(resultPage.getContent().get(1).productName()).isEqualTo("Product 2"); - - verify(auctionRepository, times(1)).findLostAuctionHistoryByUserId(userId, pageable); - } - - @Test - @DisplayName("2. 낙찰하지 못한 경매가 없는 경우 빈 목록 반환") - void getLostAuctionHistory_EmptyList() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0); - - when(auctionRepository.findLostAuctionHistoryByUserId(userId, pageable)).thenReturn(emptyPage); - - // when - Page resultPage = auctionService.getLostAuctionHistory(userId, pageable); - - // then - assertThat(resultPage).isNotNull(); - assertThat(resultPage.getContent()).isEmpty(); - assertThat(resultPage.getTotalElements()).isZero(); - - verify(auctionRepository, times(1)).findLostAuctionHistoryByUserId(userId, pageable); - } - - @Test - @DisplayName("3. 페이지네이션 동작 확인") - void getLostAuctionHistory_Pagination() { - // given - Long userId = 1L; - Pageable firstPageable = PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "endDateTime")); - Pageable secondPageable = PageRequest.of(1, 1, Sort.by(Sort.Direction.DESC, "endDateTime")); - - List allAuctions = List.of( - new LostAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, LocalDateTime.now(), 15000L), - new LostAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, LocalDateTime.now(), 25000L) - ); - - Page firstPage = new PageImpl<>(allAuctions.subList(0, 1), firstPageable, - allAuctions.size()); - Page secondPage = new PageImpl<>(allAuctions.subList(1, 2), secondPageable, - allAuctions.size()); - - when(auctionRepository.findLostAuctionHistoryByUserId(userId, firstPageable)).thenReturn(firstPage); - when(auctionRepository.findLostAuctionHistoryByUserId(userId, secondPageable)).thenReturn(secondPage); - - // when - Page firstResultPage = auctionService.getLostAuctionHistory(userId, firstPageable); - Page secondResultPage = auctionService.getLostAuctionHistory(userId, secondPageable); - - // then - assertThat(firstResultPage.getContent()).hasSize(1); - assertThat(firstResultPage.getContent().get(0).auctionId()).isEqualTo(1L); - assertThat(secondResultPage.getContent()).hasSize(1); - assertThat(secondResultPage.getContent().get(0).auctionId()).isEqualTo(2L); - - verify(auctionRepository, times(1)).findLostAuctionHistoryByUserId(userId, firstPageable); - verify(auctionRepository, times(1)).findLostAuctionHistoryByUserId(userId, secondPageable); - } - - @Test - @DisplayName("4. 정렬 순서 확인 (경매 종료 시간 내림차순)") - void getLostAuctionHistory_SortOrder() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - - LocalDateTime now = LocalDateTime.now(); - List lostAuctions = List.of( - new LostAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, now, 15000L), - new LostAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, now.minusHours(1), 25000L), - new LostAuctionResponse(3L, "Product 3", "image3.jpg", 30000, 3L, now.minusHours(2), 35000L) - ); - - Page mockPage = new PageImpl<>(lostAuctions, pageable, lostAuctions.size()); - - when(auctionRepository.findLostAuctionHistoryByUserId(userId, pageable)).thenReturn(mockPage); - - // when - Page resultPage = auctionService.getLostAuctionHistory(userId, pageable); - - // then - assertThat(resultPage.getContent()).hasSize(3); - assertThat(resultPage.getContent()).isSortedAccordingTo( - Comparator.comparing(LostAuctionResponse::endDateTime).reversed() - ); - - verify(auctionRepository, times(1)).findLostAuctionHistoryByUserId(userId, pageable); - } - - @Test - @DisplayName("5. 최고 입찰가 확인") - void getLostAuctionHistory_HighestBid() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "endDateTime")); - - List lostAuctions = List.of( - new LostAuctionResponse(1L, "Product 1", "image1.jpg", 10000, 3L, LocalDateTime.now(), 15000L), - new LostAuctionResponse(2L, "Product 2", "image2.jpg", 20000, 3L, LocalDateTime.now(), 25000L) - ); - - Page mockPage = new PageImpl<>(lostAuctions, pageable, lostAuctions.size()); - - when(auctionRepository.findLostAuctionHistoryByUserId(userId, pageable)).thenReturn(mockPage); - - // when - Page resultPage = auctionService.getLostAuctionHistory(userId, pageable); - - // then - assertThat(resultPage.getContent()).hasSize(2); - assertThat(resultPage.getContent().get(0).bidAmount()).isEqualTo(15000L); - assertThat(resultPage.getContent().get(1).bidAmount()).isEqualTo(25000L); - - verify(auctionRepository, times(1)).findLostAuctionHistoryByUserId(userId, pageable); - } - } - - @Nested - @DisplayName("낙찰 정보 조회 테스트") - class GetWinningBidTest { - @Test - @DisplayName("정상적으로 낙찰 정보를 조회한다.") - public void getWinningBidByAuctionId_Success() throws Exception { - Product product = Product.builder() - .user(User.builder().id(user.getId() + 1).build()) - .build(); - - //given - Auction auction = Auction.builder() - .id(1L) - .product(product) - .winnerId(user.getId()) - .build(); - - //when - when(auctionRepository.findById(auction.getId())).thenReturn(Optional.of(auction)); - when(auctionRepository.findWinningBidById(auction.getId())).thenReturn( - Optional.of(mock(WonAuctionDetailsResponse.class))); - - //then - assertDoesNotThrow(() -> auctionService.getWinningBidByAuctionId(user.getId(), auction.getId())); - } - - @Test - @DisplayName("사용자가 낙찰자가 아니면 AuctionException 발생") - void getWinningBidByAuctionId_NotWinner() { - // given - Auction auction = Auction.builder() - .id(1L) - .winnerId(user.getId() + 1) // user가 낙찰자가 될 수 없음 - .build(); - - when(auctionRepository.findById(auction.getId())).thenReturn(Optional.of(auction)); - - // then - assertThatThrownBy(() -> auctionService.getWinningBidByAuctionId(user.getId(), auction.getId())) - .isInstanceOf(AuctionException.class); - } - } - - private List createMockMultipartFiles() { - MultipartFile mockFile1 = new MockMultipartFile( - "testImage1.jpg", "testImage1.jpg", "image/jpeg", "test image content 1".getBytes()); - MultipartFile mockFile2 = new MockMultipartFile( - "testImage2.jpg", "testImage2.jpg", "image/jpeg", "test image content 2".getBytes()); - return List.of(mockFile1, mockFile2); - } - - private List createExistingImages(Product product) { - return List.of( - new Image(1L, "existingImage1.jpg", 1, product), - new Image(2L, "existingImage2.jpg", 2, product) - ); - } -} diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java b/src/test/java/org/chzz/market/domain/auction/service/AuctionStartServiceTest.java similarity index 78% rename from src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java rename to src/test/java/org/chzz/market/domain/auction/service/AuctionStartServiceTest.java index 18ce7995..9ab2d7d6 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionStartServiceTest.java +++ b/src/test/java/org/chzz/market/domain/auction/service/AuctionStartServiceTest.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; @@ -9,10 +9,10 @@ import java.util.List; import java.util.Optional; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.likev2.entity.LikeV2; -import org.chzz.market.domain.likev2.repository.LikeV2Repository; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.like.entity.Like; +import org.chzz.market.domain.like.repository.LikeRepository; import org.chzz.market.domain.notification.event.NotificationEvent; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,10 +24,10 @@ @ExtendWith(MockitoExtension.class) class AuctionStartServiceTest { @Mock - private AuctionV2Repository auctionRepository; + private AuctionRepository auctionRepository; @Mock - private LikeV2Repository likeRepository; + private LikeRepository likeRepository; @Mock private ApplicationEventPublisher eventPublisher; @@ -38,8 +38,8 @@ class AuctionStartServiceTest { @Test public void 사전경매에서_정식경매로_전환_성공() { // given - AuctionV2 auction = mock(AuctionV2.class); - LikeV2 like = mock(LikeV2.class); + Auction auction = mock(Auction.class); + Like like = mock(Like.class); when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); doNothing().when(auction).validateOwner(any()); @@ -56,7 +56,7 @@ class AuctionStartServiceTest { @Test public void 사전경매에서_정식경매로_전환할때_좋아요가_없을시_알림이벤트발행을_하지않는다() { // given - AuctionV2 auction = mock(AuctionV2.class); + Auction auction = mock(Auction.class); when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); doNothing().when(auction).validateOwner(any()); diff --git a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java b/src/test/java/org/chzz/market/domain/auction/service/AuctionWonServiceTest.java similarity index 61% rename from src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java rename to src/test/java/org/chzz/market/domain/auction/service/AuctionWonServiceTest.java index c368ede9..6075cc77 100644 --- a/src/test/java/org/chzz/market/domain/auctionv2/service/AuctionWonServiceTest.java +++ b/src/test/java/org/chzz/market/domain/auction/service/AuctionWonServiceTest.java @@ -1,17 +1,17 @@ -package org.chzz.market.domain.auctionv2.service; +package org.chzz.market.domain.auction.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.chzz.market.common.error.GlobalErrorCode.RESOURCE_NOT_FOUND; -import static org.chzz.market.domain.auctionv2.error.AuctionErrorCode.NOW_WINNER; +import static org.chzz.market.domain.auction.error.AuctionErrorCode.NOT_WINNER; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.util.Optional; import org.chzz.market.common.error.GlobalException; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.error.AuctionException; -import org.chzz.market.domain.auctionv2.repository.AuctionV2QueryRepository; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.error.AuctionException; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,37 +24,37 @@ class AuctionWonServiceTest { private AuctionWonService auctionWonService; @Mock - private AuctionV2Repository auctionV2Repository; + private AuctionRepository auctionRepository; @Mock - private AuctionV2QueryRepository auctionV2QueryRepository; + private AuctionQueryRepository auctionQueryRepository; @Test void 낙찰자가_아니거나_낙찰자가_존재하지않는데_조회하면_에러가_발생한다() { // given - AuctionV2 auction = AuctionV2.builder().winnerId(1L).build(); + Auction auction = Auction.builder().winnerId(1L).build(); Long userId = 2L; Long auctionId = 1L; //when - when(auctionV2Repository.findById(any())).thenReturn(Optional.of(auction)); + when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); // then assertThatThrownBy(() -> auctionWonService.getWinningBidByAuctionId(userId, auctionId)) .isInstanceOf(AuctionException.class) .extracting("errorCode") - .isEqualTo(NOW_WINNER); + .isEqualTo(NOT_WINNER); } @Test void 예기치못한_에러로_낙찰정보가_조회되지_않으면_에러가_발생한다() { // given - AuctionV2 auction = AuctionV2.builder().winnerId(1L).build(); + Auction auction = Auction.builder().winnerId(1L).build(); Long userId = 1L; Long auctionId = 1L; //when - when(auctionV2Repository.findById(any())).thenReturn(Optional.of(auction)); - when(auctionV2QueryRepository.findWinningBidById(any())).thenReturn(Optional.empty()); + when(auctionRepository.findById(any())).thenReturn(Optional.of(auction)); + when(auctionQueryRepository.findWinningBidById(any())).thenReturn(Optional.empty()); // then assertThatThrownBy(() -> auctionWonService.getWinningBidByAuctionId(userId, auctionId)) .isInstanceOf(GlobalException.class) diff --git a/src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java b/src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java deleted file mode 100644 index cab202a0..00000000 --- a/src/test/java/org/chzz/market/domain/auctionv2/controller/AuctionV2ControllerTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.chzz.market.domain.auctionv2.controller; - -import static org.hamcrest.Matchers.containsString; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.core.JsonProcessingException; -import org.chzz.market.domain.auctionv2.dto.AuctionRegisterType; -import org.chzz.market.domain.auctionv2.dto.request.RegisterRequest; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.imagev2.service.ImageV2Service; -import org.chzz.market.util.AuthenticatedRequestTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; - - -class AuctionV2ControllerTest extends AuthenticatedRequestTest { - @MockBean - ImageV2Service imageV2Service; - - RegisterRequest request; - - MockMultipartFile image1, image2, image3, image4, image5, image6; - MockMultipartFile requestPart; - - @BeforeEach - void setUp() throws JsonProcessingException { - request = new RegisterRequest("name", "description", Category.BOOKS_AND_MEDIA, 10000, - AuctionRegisterType.PRE_REGISTER); - requestPart = new MockMultipartFile( - "request", "request", "application/json", objectMapper.writeValueAsBytes(request) - ); - - image1 = new MockMultipartFile("images", "imagefile1.jpeg", "image/jpeg", - "<>".getBytes()); - image2 = new MockMultipartFile("images", "imagefile2.jpeg", "image/jpeg", - "<>".getBytes()); - - image3 = new MockMultipartFile("images", "imagefile3.jpeg", "image/jpeg", - "<>".getBytes()); - image4 = new MockMultipartFile("images", "imagefile4.jpeg", "image/jpeg", - "<>".getBytes()); - - image5 = new MockMultipartFile("images", "imagefile5.jpeg", "image/jpeg", - "<>".getBytes()); - image6 = new MockMultipartFile("images", "imagefile6.jpeg", "image/gif", - "<>".getBytes()); - } - - @Test - @DisplayName("사전 경매 등록") - void testPreAuctionRegistration() throws Exception { - // when - mockMvc.perform(multipart("/api/v2/auctions") - .file(requestPart) - .file(image1) - .file(image2) - .contentType(MediaType.MULTIPART_FORM_DATA) - .accept(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isCreated()) - .andDo(print()); - - verify(imageV2Service).uploadImages(any()); - } - - @Test - @DisplayName("이미지가 없는 경우") - void testRegisterAuctionWithNoImage() throws Exception { - - MockMultipartFile emptyImage = new MockMultipartFile( - "images", "file", MediaType.MULTIPART_FORM_DATA_VALUE, new byte[0] - ); - // when - mockMvc.perform(multipart("/api/v2/auctions") - .file(emptyImage) - .file(requestPart) - .contentType(MediaType.MULTIPART_FORM_DATA) - .accept(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message[0]").value(containsString("images: 파일은 최소 하나 이상 필요합니다."))) - .andDo(print()); - - } - - @Test - @DisplayName("이미지가 5개 이상인 경우") - void testRegisterAuctionWithOverImageCount() throws Exception { - // given - mockMvc.perform(multipart("/api/v2/auctions") - .file(requestPart) - .file(image1) - .file(image2) - .file(image3) - .file(image4) - .file(image5) - .file(image6) - .accept(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message[0]").value(containsString("images: 이미지는 5장 이내로만 업로드 가능합니다."))) - .andDo(print()); - } -} diff --git a/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java index f33df148..7c39f531 100644 --- a/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java @@ -4,10 +4,10 @@ import java.util.Comparator; import java.util.List; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.entity.Bid.BidStatus; @@ -27,7 +27,7 @@ @Transactional class BidQueryRepositoryTest { @Autowired - AuctionV2Repository auctionV2Repository; + AuctionRepository auctionRepository; @Autowired BidRepository bidRepository; @@ -46,9 +46,9 @@ class BidQueryRepositoryTest { User user4 = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); userRepository.saveAll(List.of(owner, user1, user2, user3, user4)); - AuctionV2 auction = AuctionV2.builder().seller(owner).name("맥북프로").description("맥북프로 2019년형 팝니다.") + Auction auction = Auction.builder().seller(owner).name("맥북프로").description("맥북프로 2019년형 팝니다.") .status(AuctionStatus.ENDED).category(Category.ELECTRONICS).winnerId(user1.getId()).build(); - auctionV2Repository.save(auction); + auctionRepository.save(auction); Bid bid1 = Bid.builder().bidderId(user1.getId()).auctionId(auction.getId()).amount(2000L) .status(BidStatus.ACTIVE).build(); Bid bid2 = Bid.builder().bidderId(user2.getId()).auctionId(auction.getId()).amount(1000L) @@ -76,9 +76,9 @@ class BidQueryRepositoryTest { void 해당경매_입찰내역이_아무것도_없을때_조회한다() { User owner = User.builder().email("ex").providerId("ex").providerType(ProviderType.KAKAO).build(); userRepository.save(owner); - AuctionV2 auction = AuctionV2.builder().seller(owner).name("맥북프로").description("맥북프로 2019년형 팝니다.") + Auction auction = Auction.builder().seller(owner).name("맥북프로").description("맥북프로 2019년형 팝니다.") .status(AuctionStatus.PROCEEDING).category(Category.ELECTRONICS).winnerId(null).build(); - auctionV2Repository.save(auction); + auctionRepository.save(auction); Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "bid-amount")); Page result = bidQueryRepository.findBidsByAuctionId(auction.getId(), pageable); List content = result.getContent(); diff --git a/src/test/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImplTest.java b/src/test/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImplTest.java deleted file mode 100644 index cd0e67da..00000000 --- a/src/test/java/org/chzz/market/domain/bid/repository/BidRepositoryCustomImplTest.java +++ /dev/null @@ -1,365 +0,0 @@ -package org.chzz.market.domain.bid.repository; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.chzz.market.domain.auction.type.AuctionStatus.ENDED; -import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; -import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; - -import java.time.LocalDateTime; -import java.util.Comparator; -import java.util.List; -import org.chzz.market.common.DatabaseTest; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.bid.dto.query.BiddingRecord; -import org.chzz.market.domain.bid.dto.response.BidInfoResponse; -import org.chzz.market.domain.bid.entity.Bid; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.repository.ImageRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.entity.User.ProviderType; -import org.chzz.market.domain.user.entity.User.UserRole; -import org.chzz.market.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; - -@DatabaseTest -class BidRepositoryCustomImplTest { - - @Autowired - BidRepository bidRepository; - - @Autowired - UserRepository userRepository; - - @Autowired - AuctionRepository auctionRepository; - - @Autowired - ProductRepository productRepository; - - @Autowired - ImageRepository imageRepository; - - private User bidder1, bidder2, bidder3; - private Auction auction1, auction2, auction3, auction4; - private Product product4; - - @BeforeEach - void setUp() { - bidder1 = User.builder() - .nickname("bidder1") - .providerType(ProviderType.KAKAO) - .email("aaa11@gmail.com") - .userRole(UserRole.USER) - .providerId("12314") - .build(); - - bidder2 = User.builder() - .nickname("bidder2") - .providerType(ProviderType.NAVER) - .email("bbb22@gmail.com") - .userRole(UserRole.USER) - .providerId("098327") - .build(); - - bidder3 = User.builder() - .nickname("bidder3") - .providerType(ProviderType.NAVER) - .email("bbb2233@gmail.com") - .userRole(UserRole.USER) - .providerId("0983217") - .build(); - - User seller = User.builder() - .nickname("seller") - .providerType(ProviderType.KAKAO) - .email("bbb11@gmail.com") - .userRole(UserRole.USER) - .providerId("2222") - .build(); - - userRepository.saveAll(List.of(seller, bidder1, bidder2, bidder3)); - - Product product1 = Product.builder() - .category(Category.OTHER) - .description("asd") - .minPrice(1000) - .name("asd") - .user(seller) - .build(); - - Product product2 = Product.builder() - .category(Category.OTHER) - .description("asd") - .minPrice(1000) - .name("asd") - .user(seller) - .build(); - - Product product3 = Product.builder() - .category(Category.OTHER) - .description("product3") - .name("product3") - .minPrice(100000) - .user(seller) - .build(); - - product4 = Product.builder() - .category(Category.OTHER) - .description("product4") - .name("product4") - .minPrice(100000) - .user(seller) - .build(); - - productRepository.saveAll(List.of(product1, product2, product3, product4)); - - Image image1 = Image.builder() - .product(product1) - .cdnPath("qepifnv2") - .build(); - Image image2 = Image.builder() - .product(product1) - .cdnPath("rrreww4") - .build(); - - imageRepository.saveAll(List.of(image1, image2)); - - auction1 = Auction.builder() - .product(product1) - .endDateTime(LocalDateTime.now().plusDays(2)) - .status(PROCEEDING) - .winnerId(2L) - .build(); - - auction2 = Auction.builder() - .product(product2) - .endDateTime(LocalDateTime.now().plusDays(1)) - .status(PROCEEDING) - .winnerId(2L) - .build(); - - auction3 = Auction.builder() - .product(product3) - .status(ENDED) - .endDateTime(LocalDateTime.now()) - .winnerId(null) // 낙찰자가 없는 경우 - .build(); - - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - Bid bid1 = Bid.builder() - .amount(1000L) - .auctionId(auction1.getId()) - .count(2) - .bidderId(bidder1.getId()) - .build(); - Bid bid2 = Bid.builder() - .amount(2000L) - .auctionId(auction2.getId()) - .count(3) - .bidderId(bidder1.getId()) - .build(); - Bid bid3 = Bid.builder() - .amount(300000L) - .auctionId(auction1.getId()) - .count(1) - .bidderId(bidder2.getId()) - .build(); - Bid bid4 = Bid.builder() - .amount(10000000L) - .auctionId(auction2.getId()) - .count(1) - .bidderId(bidder2.getId()) - .build(); - - Bid cancelledBid1 = Bid.builder() - .amount(10000000L) - .auctionId(auction3.getId()) - .count(1) - .bidderId(bidder2.getId()) - .status(CANCELLED) - .build(); - Bid cancelledBid2 = Bid.builder() - .amount(10000000L) - .auctionId(auction3.getId()) - .count(1) - .bidderId(bidder1.getId()) - .status(CANCELLED) - .build(); - - bidRepository.saveAll(List.of(bid1, bid2, bid3, bid4, cancelledBid1, cancelledBid2)); - } - - @Test - @DisplayName("입찰 기록은 가격 기준으로 정렬 가능하다") - void testFindBidHistory() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("bidAmount")); - Page usersBidHistory1 = bidRepository.findUsersBidHistory(bidder1.getId(), pageable, null); - Page usersBidHistory2 = bidRepository.findUsersBidHistory(bidder2.getId(), pageable, null); - - // when - - // then - assertThat(usersBidHistory1.isEmpty()).isFalse(); - assertThat(usersBidHistory1.getTotalPages()).isEqualTo(1); - assertThat(usersBidHistory1.getNumberOfElements()).isEqualTo(2); - assertThat(usersBidHistory1.getContent()).isSortedAccordingTo( - Comparator.comparing(BiddingRecord::getBidAmount)); - - assertThat(usersBidHistory2.isEmpty()).isFalse(); - assertThat(usersBidHistory2.getTotalPages()).isEqualTo(1); - assertThat(usersBidHistory2.getNumberOfElements()).isEqualTo(2); - assertThat(usersBidHistory2.getContent()).isSortedAccordingTo( - Comparator.comparing(BiddingRecord::getBidAmount)); - } - - @Test - @DisplayName("입찰 기록은 남은 시간 기준으로 정렬 가능하다") - void testFindBidHistoryOrderByTimeRemaining() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("time-remaining")); - Page usersBidHistory1 = bidRepository.findUsersBidHistory(bidder1.getId(), pageable, null); - Page usersBidHistory2 = bidRepository.findUsersBidHistory(bidder2.getId(), pageable, null); - // when - - // then - assertThat(usersBidHistory1.isEmpty()).isFalse(); - assertThat(usersBidHistory1.getTotalPages()).isEqualTo(1); - assertThat(usersBidHistory1.getNumberOfElements()).isEqualTo(2); - assertThat(usersBidHistory1.getContent()).isSortedAccordingTo( - Comparator.comparing(BiddingRecord::getTimeRemaining).reversed()); - - assertThat(usersBidHistory2.isEmpty()).isFalse(); - assertThat(usersBidHistory2.getTotalPages()).isEqualTo(1); - assertThat(usersBidHistory2.getNumberOfElements()).isEqualTo(2); - assertThat(usersBidHistory2.getContent()).isSortedAccordingTo( - Comparator.comparing(BiddingRecord::getTimeRemaining).reversed()); - - } - - @Test - @DisplayName("경매와 관련된 모든 입찰을 금액 내림차순으로 조회할 수 있다") - void testFindAllBidsByAuction() { - // given & when - List bidsForAuction1 = bidRepository.findAllBidsByAuction(auction1); - List bidsForAuction2 = bidRepository.findAllBidsByAuction(auction2); - - // then - // Auction 1 테스트 - assertThat(bidsForAuction1).hasSize(2); - assertThat(bidsForAuction1.get(0).getAmount()).isEqualTo(300000L); - assertThat(bidsForAuction1.get(1).getAmount()).isEqualTo(1000L); - - // Auction 2 테스트 - assertThat(bidsForAuction2).hasSize(2); - assertThat(bidsForAuction2.get(0).getAmount()).isEqualTo(10000000L); - assertThat(bidsForAuction2.get(1).getAmount()).isEqualTo(2000L); - } - - @Test - @DisplayName("경매에 입찰이 없을 경우 빈 리스트를 반환한다") - void testFindAllBidsByAuctionNoBids() { - // given - Product product3 = Product.builder() - .category(Category.OTHER) - .description("product3") - .name("product3") - .minPrice(100000) - .user(bidder1) - .build(); - - Auction auction3 = Auction.builder() - .product(product3) - .status(PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(2)) - .build(); - - productRepository.save(product3); - auctionRepository.save(auction3); - - // when - List bidsForAuction3 = bidRepository.findAllBidsByAuction(auction3); - - // then - assertThat(bidsForAuction3).isEmpty(); - } - - @Test - @DisplayName("경매 ID로 입찰 내역을 조회할 때 활성화 된 입찰이 없는 경우(낙찰자가 없는 경우)를 처리한다") - void testFindBidsByAuctionIdWithoutWinner() { - // given - Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "bid-amount")); - - // when - Page bidsForAuction3 = bidRepository.findBidsByAuctionId(auction3.getId(), pageable); - - // then - assertThat(bidsForAuction3.getContent()).hasSize(0); - assertThat(bidsForAuction3.getTotalPages()).isEqualTo(0); - } - - @Test - @DisplayName("경매 ID로 입찰 내역을 조회할 때 낙찰자가 있는 경우를 처리한다") - void testFindBidsByAuctionIdWithWinner() { - auction4 = Auction.builder() - .product(product4) - .status(ENDED) - .endDateTime(LocalDateTime.now()) - .winnerId(bidder2.getId()) // 낙찰자가 있는 경우 - .build(); - auctionRepository.save(auction4); - Bid bid5 = Bid.builder() - .amount(5000L) - .auctionId(auction4.getId()) - .count(1) - .bidderId(bidder1.getId()) - .build(); - - Bid bid6 = Bid.builder() - .amount(7000L) - .auctionId(auction4.getId()) - .count(2) - .bidderId(bidder2.getId()) - .build(); - Bid cancelledBid3 = Bid.builder() - .amount(10000L) - .auctionId(auction4.getId()) - .count(3) - .bidderId(bidder3.getId()) - .status(CANCELLED) - .build(); - bidRepository.saveAll(List.of(bid5, bid6, cancelledBid3)); - - // given - Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "bid-amount")); - - // when - Page bidsForAuction4 = bidRepository.findBidsByAuctionId(auction4.getId(), pageable); - - BidInfoResponse winningBid = bidsForAuction4.getContent().stream() - .filter(BidInfoResponse::isWinningBidder) - .findFirst() - .orElse(null); - // then - assertThat(bidsForAuction4.getContent()).hasSize(2); // 활성화된 입찰 2개만 조회되어야 함 - assertThat(bidsForAuction4.getContent()).isSortedAccordingTo( - Comparator.comparing(BidInfoResponse::bidAmount).reversed()); - assertThat(winningBid).isNotNull(); - assertThat(winningBid.bidAmount()).isEqualTo(7000L); - assertThat(winningBid.bidderNickname()).isEqualTo("bidder2"); - assertThat(winningBid.isWinningBidder()).isTrue(); - } - -} diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java index 488231e5..8080dae2 100644 --- a/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java +++ b/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java @@ -11,15 +11,15 @@ import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.bid.error.BidErrorCode; import org.chzz.market.domain.bid.error.BidException; import org.chzz.market.domain.bid.repository.BidRepository; -import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.image.entity.Image; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; @@ -34,7 +34,7 @@ class BidCancelLockServiceTest { private BidCancelLockService bidCancelLockService; @Autowired - private AuctionV2Repository auctionRepository; + private AuctionRepository auctionRepository; @Autowired private BidRepository bidRepository; @@ -42,17 +42,17 @@ class BidCancelLockServiceTest { @Autowired private UserRepository userRepository; - private AuctionV2 auction; + private Auction auction; private User seller; private List users; private List bids; - private ImageV2 defaultImage; + private Image defaultImage; @BeforeEach public void setUp() { seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); userRepository.save(seller); - defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + defaultImage = Image.builder().cdnPath("https://cdn.com").sequence(1).build(); users = IntStream.range(1, 6) .mapToObj(i -> User.builder() .email("user" + i + "@example.com") @@ -97,7 +97,7 @@ public void multipleUsersCancelBidTest() throws InterruptedException { executorService.shutdown(); // Auction 업데이트 후 결과 검증 - AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + Auction updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); long bidCount = updatedAuction.getBidCount(); // 모든 입찰 취소 후 카운트 0 검증 @@ -138,13 +138,13 @@ public void singleUserConcurrentCancelBidTest_ThrowsException() throws Interrupt .isEqualTo(BidErrorCode.BID_ALREADY_CANCELLED); // 최종 입찰 수 확인 (4가 되어야 함) - AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + Auction updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); long bidCount = updatedAuction.getBidCount(); assertThat(bidCount).isEqualTo(4); } - private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { - AuctionV2 auction = AuctionV2.builder() + private Auction createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + Auction auction = Auction.builder() .seller(seller) .name(name) .description(description) diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java index bb43ad2f..da724efc 100644 --- a/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java +++ b/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java @@ -11,14 +11,14 @@ import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.bid.dto.BidCreateRequest; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.bid.dto.request.BidCreateRequest; import org.chzz.market.domain.bid.error.BidErrorCode; import org.chzz.market.domain.bid.error.BidException; -import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.image.entity.Image; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; @@ -33,21 +33,21 @@ public class BidCreateServiceConcurrencyTest { private BidCreateService bidCreateService; @Autowired - private AuctionV2Repository auctionRepository; + private AuctionRepository auctionRepository; @Autowired private UserRepository userRepository; - private AuctionV2 auction; + private Auction auction; private User seller; private List users; - private ImageV2 defaultImage; + private Image defaultImage; @BeforeEach public void setUp() { seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); userRepository.save(seller); - defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + defaultImage = Image.builder().cdnPath("https://cdn.com").sequence(1).build(); auction = auctionRepository.save( createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null)); users = IntStream.range(1, 6) @@ -81,7 +81,7 @@ public void setUp() { latch.await(); executorService.shutdown(); - AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + Auction updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); long bidCount = updatedAuction.getBidCount(); assertThat(bidCount).isEqualTo(numberOfThreads); } @@ -119,13 +119,13 @@ public void setUp() { .isEqualTo(BidErrorCode.BID_SAME_AS_PREVIOUS); // 최종 입찰 수 확인 (1번만 성공) - AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); + Auction updatedAuction = auctionRepository.findById(auction.getId()).orElseThrow(); long bidCount = updatedAuction.getBidCount(); assertThat(bidCount).isEqualTo(1); } - private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { - AuctionV2 auction = AuctionV2.builder() + private Auction createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + Auction auction = Auction.builder() .seller(seller) .name(name) .description(description) diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidServiceTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidServiceTest.java deleted file mode 100644 index 2c402839..00000000 --- a/src/test/java/org/chzz/market/domain/bid/service/BidServiceTest.java +++ /dev/null @@ -1,324 +0,0 @@ -package org.chzz.market.domain.bid.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.chzz.market.domain.auction.error.AuctionErrorCode.AUCTION_ENDED; -import static org.chzz.market.domain.bid.entity.Bid.BidStatus.CANCELLED; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_ALREADY_CANCELLED; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BELOW_MIN_PRICE; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_BY_OWNER; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_LIMIT_EXCEEDED; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_NOT_ACCESSIBLE; -import static org.chzz.market.domain.bid.error.BidErrorCode.BID_SAME_AS_PREVIOUS; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.Mockito.when; - -import java.time.LocalDateTime; -import java.util.Optional; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.error.AuctionErrorCode; -import org.chzz.market.domain.auction.error.AuctionException; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.bid.dto.BidCreateRequest; -import org.chzz.market.domain.bid.entity.Bid; -import org.chzz.market.domain.bid.error.BidException; -import org.chzz.market.domain.bid.repository.BidRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -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.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -@ExtendWith(MockitoExtension.class) -class BidServiceTest { - private static final String ERROR_CODE = "errorCode"; - - @Mock - private AuctionRepository auctionRepository; - - @Mock - private BidRepository bidRepository; - - @Mock - private UserRepository userRepository; - - @InjectMocks - private BidService bidService; - - private BidCreateRequest bidCreateRequest; - private User user, user2, user3; - private Product product, product2, product3; - private Auction auction, completeAuction, endAuction; - - @BeforeEach - void setUp() { - user = User.builder().id(1L).providerId("1234").nickname("닉네임1").email("asd@naver.com").build(); - user2 = User.builder().id(2L).providerId("12345").nickname("닉네임2").email("asd@naver.com").build(); - user3 = User.builder().id(3L).providerId("123").nickname("닉네임").email("as@naver.com").build(); - product = Product.builder().user(user).name("제품1").category(Category.FASHION_AND_CLOTHING).minPrice(1000) - .build(); - product2 = Product.builder().user(user).name("제품2").category(Category.FASHION_AND_CLOTHING).minPrice(1000) - .build(); - product3 = Product.builder().user(user).name("제품3").category(Category.FASHION_AND_CLOTHING).minPrice(1000) - .build(); - auction = Auction.builder().id(1L).product(product).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - completeAuction = Auction.builder().id(2L).product(product2).status(AuctionStatus.ENDED) - .endDateTime(LocalDateTime.now().minusDays(1)).build(); - endAuction = Auction.builder().id(3L).product(product3).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().minusDays(1)).build(); - } - - @Test - @DisplayName("성공 - 처음 입찰 한 경우") - public void firstBid_Success() throws Exception { - //given - bidCreateRequest = BidCreateRequest.builder().auctionId(1L).bidAmount(1000L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(auction)); - when(bidRepository.findByAuctionIdAndBidderId(auction.getId(), user2.getId())).thenReturn(Optional.empty()); - - //when & then - assertDoesNotThrow(() -> bidService.createBid(bidCreateRequest, 2L)); - } - - @Test - @DisplayName("성공 - 이미 입찰 한 경우 업데이트") - public void updateBid_Success() throws Exception { - //given - bidCreateRequest = BidCreateRequest.builder().auctionId(1L).bidAmount(2000L).build(); - Bid bid = Bid.builder().id(1L).auctionId(auction.getId()).bidderId(user2.getId()).amount(1000L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(auction)); - when(bidRepository.findByAuctionIdAndBidderId(auction.getId(), user2.getId())).thenReturn(Optional.of(bid)); - - //when - bidService.createBid(bidCreateRequest, 2L); - - //then - assertThat(bid.getId()).isEqualTo(1L); - assertThat(bid.getAmount()).isEqualTo(2000L); - assertThat(bid.getCount()).isEqualTo(1L); - } - - @Test - @DisplayName("실패 - 경매 등록자가 입찰할 때 예외 발생") - public void ownerBid_ThrowsException() throws Exception { - // given - bidCreateRequest = BidCreateRequest.builder().auctionId(1L).bidAmount(1000L).build(); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(auction)); - - // when & then - assertThatThrownBy(() -> bidService.createBid(bidCreateRequest, 1L)) - .isInstanceOf(BidException.class) - .extracting(ERROR_CODE) - .isEqualTo(BID_BY_OWNER); - } - - @Test - @DisplayName("실패 - 경매가 상태가 진행이 아닐 때 예외 발생") - public void notProceeding_ThrowsException() throws Exception { - //given - bidCreateRequest = BidCreateRequest.builder().auctionId(2L).bidAmount(1000L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn( - Optional.ofNullable(completeAuction)); - - //when & then - assertThatThrownBy(() -> bidService.createBid(bidCreateRequest, 2L)) - .isInstanceOf(AuctionException.class) - .extracting(ERROR_CODE) - .isEqualTo(AUCTION_ENDED); - - } - - @Test - @DisplayName("실패 - 입찰 시각이 종료시각을 지날 때 예외 발생") - public void auctionEnded_ThrowsException() throws Exception { - //given - bidCreateRequest = BidCreateRequest.builder().auctionId(3L).bidAmount(1000L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(endAuction)); - - //when & then - assertThatThrownBy(() -> bidService.createBid(bidCreateRequest, 2L)) - .isInstanceOf(AuctionException.class) - .extracting(ERROR_CODE) - .isEqualTo(AUCTION_ENDED); - } - - @Test - @DisplayName("실패 - 최소 금액보다 낮은 입찰 금액일때 예외 발생") - public void bidBelowMinPrice_ThrowsException() throws Exception { - //given - bidCreateRequest = BidCreateRequest.builder().auctionId(1L).bidAmount(500L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(auction)); - - //when & then - assertThatThrownBy(() -> bidService.createBid(bidCreateRequest, 2L)) - .isInstanceOf(BidException.class) - .extracting(ERROR_CODE) - .isEqualTo(BID_BELOW_MIN_PRICE); - } - - @Test - @DisplayName("실패 - 남은 입찰 횟수가 0보다 작을 때 입찰 한 경우 예외 발생") - public void bidCountZeroOrLess_ThrowsException() throws Exception { - //given - Bid bid = Bid.builder().id(1L).auctionId(auction.getId()).bidderId(user2.getId()).amount(1000L).count(0) - .build(); - bidCreateRequest = BidCreateRequest.builder().auctionId(1L).bidAmount(5000L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(auction)); - when(bidRepository.findByAuctionIdAndBidderId(auction.getId(), user2.getId())).thenReturn(Optional.of(bid)); - - //when & then - assertThatThrownBy(() -> bidService.createBid(bidCreateRequest, 2L)) - .isInstanceOf(BidException.class) - .extracting(ERROR_CODE) - .isEqualTo(BID_LIMIT_EXCEEDED); - } - - @Test - @DisplayName("실패 - 기존 입찰 금액과 동일한 입찰 금액인 경우") - public void asd() throws Exception { - //given - bidCreateRequest = BidCreateRequest.builder().auctionId(1L).bidAmount(1000L).build(); - Bid bid = Bid.builder().id(1L).auctionId(auction.getId()).bidderId(user2.getId()).amount(1000L).build(); - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); - when(auctionRepository.findById(bidCreateRequest.getAuctionId())).thenReturn(Optional.ofNullable(auction)); - when(bidRepository.findByAuctionIdAndBidderId(auction.getId(), user2.getId())).thenReturn(Optional.of(bid)); - - //when & then - assertThatThrownBy(() -> bidService.createBid(bidCreateRequest, 2L)) - .isInstanceOf(BidException.class) - .extracting(ERROR_CODE) - .isEqualTo(BID_SAME_AS_PREVIOUS); - } - - @Test - @DisplayName("성공 - 입찰 취소") - public void cancelBid_Success() throws Exception { - //given - Bid bid = Bid.builder().id(1L).auctionId(auction.getId()).bidderId(user2.getId()).amount(1000L).count(3) - .build(); - - //when - when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); - when(bidRepository.findById(1L)).thenReturn(Optional.of(bid)); - bidService.cancelBid(bid.getId(), user2.getId()); - //then - assertThat(bid.getStatus()).isEqualTo(CANCELLED); - } - - @Test - @DisplayName("실패 - 입찰 취소 시 입찰자가 아닌 경우 예외 발생") - public void cancelBid_NotByBidder_ThrowsException() throws Exception { - //given - Bid bid = Bid.builder().id(1L).auctionId(auction.getId()).bidderId(user2.getId()).amount(1000L).count(3) - .build(); - - //when - when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); - when(bidRepository.findById(1L)).thenReturn(Optional.of(bid)); - - //then - assertThatThrownBy(() -> bidService.cancelBid(1L, 3L)) - .isInstanceOf(BidException.class) - .extracting(ERROR_CODE) - .isEqualTo(BID_NOT_ACCESSIBLE); - } - - @Test - @DisplayName("실패 - 입찰 취소 시각이 경매 종료 시각을 지날 때 예외 발생") - public void cancelBid_AfterAuctionEnded_ThrowsException() throws Exception { - //given - Bid bid = Bid.builder().id(3L).auctionId(endAuction.getId()).bidderId(user2.getId()).amount(1000L).count(3) - .build(); - - //when - when(auctionRepository.findById(3L)).thenReturn(Optional.of(endAuction)); - when(bidRepository.findById(3L)).thenReturn(Optional.of(bid)); - - //then - assertThatThrownBy(() -> bidService.cancelBid(3L, 2L)) - .isInstanceOf(AuctionException.class) - .extracting(ERROR_CODE) - .isEqualTo(AUCTION_ENDED); - } - - @Test - @DisplayName("실패 - 이미 취소된 입찰을 취소할 때 예외 발생") - public void cancelBid_AlreadyCancelledBid_ThrowsException() throws Exception { - //given - Bid bid = Bid.builder().id(1L).auctionId(auction.getId()).bidderId(user2.getId()).amount(1000L).count(3) - .status(CANCELLED).build(); - - //when - when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); - when(bidRepository.findById(1L)).thenReturn(Optional.of(bid)); - - //then - assertThatThrownBy(() -> bidService.cancelBid(1L, 2L)) - .isInstanceOf(BidException.class) - .extracting(ERROR_CODE) - .isEqualTo(BID_ALREADY_CANCELLED); - } - - @Test - @DisplayName("실패 - 사용자가 경매 소유자가 아닌 경우 예외 발생") - void getBidsByAuctionId_ThrowsForbiddenAuctionAccessException() { - // given - Long auctionId = 1L; - Long userId = 2L; // 경매 소유자와 다른 사용자 ID - Pageable pageable = PageRequest.of(0, 10); - Auction auction = Auction.builder() - .id(auctionId) - .product(product) - .status(AuctionStatus.ENDED) - .endDateTime(LocalDateTime.now().minusDays(1)) - .build(); - - when(auctionRepository.findById(auctionId)).thenReturn(Optional.of(auction)); - - // when & then - assertThatThrownBy(() -> bidService.getBidsByAuctionId(userId, auctionId, pageable)) - .isInstanceOf(AuctionException.class) - .extracting(ERROR_CODE) - .isEqualTo(AuctionErrorCode.FORBIDDEN_AUCTION_ACCESS); - } - - @Test - @DisplayName("실패 - 경매가 종료되지 않은 경우 예외 발생") - void getBidsByAuctionId_ThrowsAuctionNotEndedException() { - // given - Long auctionId = 1L; - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 10); - Auction auction = Auction.builder() - .id(auctionId) - .product(product) - .status(AuctionStatus.PROCEEDING) // 경매 진행 중 - .endDateTime(LocalDateTime.now().plusDays(1)) - .build(); - - when(auctionRepository.findById(auctionId)).thenReturn(Optional.of(auction)); - - // when & then - assertThatThrownBy(() -> bidService.getBidsByAuctionId(userId, auctionId, pageable)) - .isInstanceOf(AuctionException.class) - .extracting(ERROR_CODE) - .isEqualTo(AuctionErrorCode.AUCTION_NOT_ENDED); - } -} diff --git a/src/test/java/org/chzz/market/domain/imagev2/service/ImageDeleteServiceTest.java b/src/test/java/org/chzz/market/domain/imagev2/service/ImageDeleteServiceTest.java index f3453c17..a1409c23 100644 --- a/src/test/java/org/chzz/market/domain/imagev2/service/ImageDeleteServiceTest.java +++ b/src/test/java/org/chzz/market/domain/imagev2/service/ImageDeleteServiceTest.java @@ -1,7 +1,7 @@ package org.chzz.market.domain.imagev2.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.chzz.market.domain.imagev2.error.ImageErrorCode.IMAGE_DELETE_FAILED; +import static org.chzz.market.domain.image.error.ImageErrorCode.IMAGE_DELETE_FAILED; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -11,8 +11,9 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; import java.util.List; -import org.chzz.market.domain.image.entity.ImageV2; -import org.chzz.market.domain.imagev2.error.exception.ImageException; +import org.chzz.market.domain.image.entity.Image; +import org.chzz.market.domain.image.error.exception.ImageException; +import org.chzz.market.domain.image.service.ImageDeleteService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,8 +40,8 @@ void setUp() { @Test void 이미지_삭제_성공() { // given - ImageV2 image1 = mock(ImageV2.class); - ImageV2 image2 = mock(ImageV2.class); + Image image1 = mock(Image.class); + Image image2 = mock(Image.class); when(image1.getCdnPath()).thenReturn(cdnPath1); when(image2.getCdnPath()).thenReturn(cdnPath2); @@ -56,7 +57,7 @@ void setUp() { @Test void S3_에러로_이미지삭제시_예외발생() { // given - ImageV2 image = mock(ImageV2.class); + Image image = mock(Image.class); when(image.getCdnPath()).thenReturn(cdnPath1); doThrow(AmazonServiceException.class).when(amazonS3Client).deleteObject(bucket, "image1.jpg"); @@ -71,7 +72,7 @@ void setUp() { @Test void 잘못된_URL_이미지_삭제시_예외발생() { // given - ImageV2 image = mock(ImageV2.class); + Image image = mock(Image.class); when(image.getCdnPath()).thenReturn("invalid-url"); // when & then diff --git a/src/test/java/org/chzz/market/domain/like/service/LikeServiceTest.java b/src/test/java/org/chzz/market/domain/like/service/LikeServiceTest.java deleted file mode 100644 index 051a5e25..00000000 --- a/src/test/java/org/chzz/market/domain/like/service/LikeServiceTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.chzz.market.domain.like.service; - -import org.chzz.market.domain.like.dto.LikeResponse; -import org.chzz.market.domain.like.entity.Like; -import org.chzz.market.domain.like.repository.LikeRepository; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class LikeServiceTest { - - @Mock - private LikeRepository likeRepository; - - @Mock - private ProductRepository productRepository; - - @Mock - private UserRepository userRepository; - - @InjectMocks - private LikeService likeService; - - private User user, user2; - private Product product; - private Like like; - - @BeforeEach - void setUp() { - user = User.builder() - .id(1L) - .build(); - - user2 = User.builder() - .id(2L) - .build(); - - product = Product.builder() - .id(1L) - .build(); - - like = Like.builder() - .product(product) - .user(user) - .id(1L) - .build(); - } - - @Nested - @DisplayName("좋아요 토글 테스트") - class ToggleLikeTest { - - @Test - @DisplayName("1. 좋아요 없을 때 좋아요 생성") - void createNewLikeWhenNotExists() { - // given - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(productRepository.findPreOrder(1L)).thenReturn(Optional.of(product)); - when(likeRepository.existsByUserIdAndProductId(1L, 1L)).thenReturn(false); - - // when - LikeResponse response = likeService.toggleLike(1L, 1L); - - // then - assertTrue(response.isLiked()); - assertEquals(1, product.getLikeCount()); - verify(likeRepository).save(any(Like.class)); - } - - @Test - @DisplayName("2. 좋아요 있을 때 좋아요 삭제") - void removeLikeWhenExists() { - // then - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(productRepository.findPreOrder(1L)).thenReturn(Optional.of(product)); - when(likeRepository.existsByUserIdAndProductId(1L, 1L)).thenReturn(true); - when(likeRepository.findByUserAndProduct(user, product)).thenReturn(Optional.of(like)); - - // when - LikeResponse response = likeService.toggleLike(1L, 1L); - - // then - assertFalse(response.isLiked()); - assertEquals(0, product.getLikeCount()); - verify(likeRepository).delete(any(Like.class)); - } - - @Test - @DisplayName("3. 여러 사용자가 좋아요 누르면 좋아요 수가 증가") - void increaseLikeCountWhenMultipleUsersLike() { - // given - when(userRepository.findById(anyLong())).thenReturn(Optional.of(user), Optional.of(user2)); - when(productRepository.findPreOrder(1L)).thenReturn(Optional.of(product)); - when(likeRepository.existsByUserIdAndProductId(anyLong(), anyLong())).thenReturn(false); - - // when - likeService.toggleLike(1L, 1L); - LikeResponse response = likeService.toggleLike(2L, 1L); - - // then - assertTrue(response.isLiked()); - assertEquals(2, product.getLikeCount()); - verify(likeRepository, times(2)).save(any(Like.class)); - } - } -} \ No newline at end of file diff --git a/src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java b/src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java similarity index 74% rename from src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java rename to src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java index 7f6a14c8..c82a58df 100644 --- a/src/test/java/org/chzz/market/domain/likev2/service/LikeUpdateServiceConcurrencyTest.java +++ b/src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java @@ -1,15 +1,15 @@ -package org.chzz.market.domain.likev2.service; +package org.chzz.market.domain.like.service; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.chzz.market.domain.auctionv2.entity.AuctionStatus; -import org.chzz.market.domain.auctionv2.entity.AuctionV2; -import org.chzz.market.domain.auctionv2.entity.Category; -import org.chzz.market.domain.auctionv2.repository.AuctionV2Repository; -import org.chzz.market.domain.image.entity.ImageV2; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.Category; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.image.entity.Image; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; @@ -26,24 +26,24 @@ public class LikeUpdateServiceConcurrencyTest { private LikeUpdateService likeUpdateService; @Autowired - private AuctionV2Repository auctionRepository; + private AuctionRepository auctionRepository; private User seller; private User user; - private ImageV2 defaultImage; + private Image defaultImage; @BeforeEach void setUp() { seller = User.builder().email("seller").providerId("seller").providerType(User.ProviderType.KAKAO).build(); user = User.builder().email("user").providerId("user").providerType(User.ProviderType.KAKAO).build(); - defaultImage = ImageV2.builder().cdnPath("https://cdn.com").sequence(1).build(); + defaultImage = Image.builder().cdnPath("https://cdn.com").sequence(1).build(); userRepository.save(seller); userRepository.save(user); } @Test public void 좋아요_동시성_테스트() throws InterruptedException { - AuctionV2 auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); + Auction auction = createAuction(seller, "맥북프로", "맥북프로 2019년형 팝니다.", AuctionStatus.PROCEEDING, null); int numberOfThreads = 10; ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); @@ -64,7 +64,7 @@ void setUp() { latch.await(); executorService.shutdown(); - AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()) + Auction updatedAuction = auctionRepository.findById(auction.getId()) .orElseThrow(() -> new RuntimeException("Auction not found")); assertThat(updatedAuction.getLikeCount()).isEqualTo(10); } @@ -72,7 +72,7 @@ void setUp() { @Test public void 한사람_동시에_여러_좋아요_요청_테스트() throws InterruptedException { // 경매 생성 - AuctionV2 auction = createAuction(seller, "아이폰 13", "최신형 아이폰 13 팝니다.", AuctionStatus.PROCEEDING, null); + Auction auction = createAuction(seller, "아이폰 13", "최신형 아이폰 13 팝니다.", AuctionStatus.PROCEEDING, null); int numberOfThreads = 9; // 동시에 요청할 스레드 수 ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); @@ -96,15 +96,15 @@ void setUp() { executorService.shutdown(); // THEN - AuctionV2 updatedAuction = auctionRepository.findById(auction.getId()) + Auction updatedAuction = auctionRepository.findById(auction.getId()) .orElseThrow(() -> new RuntimeException("Auction not found")); assertThat(updatedAuction.getLikeCount()).isEqualTo(1); } - private AuctionV2 createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { - AuctionV2 auction = AuctionV2.builder() + private Auction createAuction(User seller, String name, String description, AuctionStatus status, Long winnerId) { + Auction auction = Auction.builder() .seller(seller) .name(name) .description(description) diff --git a/src/test/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImplTest.java b/src/test/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImplTest.java deleted file mode 100644 index 39c9a58c..00000000 --- a/src/test/java/org/chzz/market/domain/product/repository/ProductRepositoryCustomImplTest.java +++ /dev/null @@ -1,528 +0,0 @@ -package org.chzz.market.domain.product.repository; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.chzz.market.domain.product.entity.Product.Category.BOOKS_AND_MEDIA; -import static org.chzz.market.domain.product.entity.Product.Category.ELECTRONICS; -import static org.chzz.market.domain.product.entity.Product.Category.FASHION_AND_CLOTHING; -import static org.chzz.market.domain.product.entity.Product.Category.HOME_APPLIANCES; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.time.LocalDateTime; -import java.util.Comparator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; -import org.chzz.market.common.DatabaseTest; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.repository.ImageRepository; -import org.chzz.market.domain.like.entity.Like; -import org.chzz.market.domain.like.repository.LikeRepository; -import org.chzz.market.domain.product.dto.ProductDetailsResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.entity.Product.Category; -import org.chzz.market.domain.user.entity.User; -import org.chzz.market.domain.user.repository.UserRepository; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.transaction.annotation.Transactional; - -@DatabaseTest -@Transactional -class ProductRepositoryCustomImplTest { - - @Autowired - ProductRepository productRepository; - - @Autowired - UserRepository userRepository; - - @Autowired - ImageRepository imageRepository; - - @Autowired - LikeRepository likeRepository; - - @Autowired - AuctionRepository auctionRepository; - - @PersistenceContext - EntityManager entityManager; - - private static User user1, user2, user3; - private static Product product1, product2, product3, product4, product5, product6; - private static Image image1, image2, image3, image4, image5, image6; - private static Like like1, like2, like3; - private static Auction auction1; - - @BeforeAll - static void setUpOnce(@Autowired UserRepository userRepository, - @Autowired ProductRepository productRepository, - @Autowired AuctionRepository auctionRepository, - @Autowired ImageRepository imageRepository, - @Autowired LikeRepository likeRepository) { - user1 = User.builder().providerId("1234").nickname("닉네임1").email("user1@test.com").build(); - user2 = User.builder().providerId("5678").nickname("닉네임2").email("user2@test.com").build(); - user3 = User.builder().providerId("9012").nickname("닉네임3").email("user3@test.com").build(); - userRepository.saveAll(List.of(user1, user2, user3)); - - product1 = Product.builder().user(user1).name("사전등록상품1").category(ELECTRONICS).minPrice(10000).build(); - ReflectionTestUtils.setField(product1, "createdAt", LocalDateTime.now().minusDays(5)); - product2 = Product.builder().user(user1).name("사전등록상품2").category(BOOKS_AND_MEDIA).minPrice(20000).build(); - ReflectionTestUtils.setField(product2, "createdAt", LocalDateTime.now().minusDays(4)); - product3 = Product.builder().user(user2).name("사전등록상품3").category(ELECTRONICS).minPrice(30000).build(); - ReflectionTestUtils.setField(product3, "createdAt", LocalDateTime.now().minusDays(3)); - product4 = Product.builder().user(user2).name("사전등록상품4").category(ELECTRONICS).minPrice(40000).build(); - ReflectionTestUtils.setField(product4, "createdAt", LocalDateTime.now().minusDays(2)); - product5 = Product.builder().user(user3).name("사전등록상품5").category(FASHION_AND_CLOTHING).minPrice(50000).build(); - ReflectionTestUtils.setField(product5, "createdAt", LocalDateTime.now().minusDays(1)); - product6 = Product.builder().user(user3).name("사전등록상품6").category(FASHION_AND_CLOTHING).minPrice(50000).build(); - productRepository.saveAll(List.of(product1, product2, product3, product4, product5, product6)); - - image1 = Image.builder().product(product1).cdnPath("path/to/image1.jpg").sequence(1).build(); - image2 = Image.builder().product(product2).cdnPath("path/to/image2.jpg").sequence(1).build(); - image3 = Image.builder().product(product3).cdnPath("path/to/image3.jpg").sequence(1).build(); - image4 = Image.builder().product(product4).cdnPath("path/to/image4.jpg").sequence(1).build(); - image5 = Image.builder().product(product5).cdnPath("path/to/image5.jpg").sequence(1).build(); - image6 = Image.builder().product(product6).cdnPath("path/to/image6.jpg").sequence(1).build(); - imageRepository.saveAll(List.of(image1, image2, image3, image4, image5, image6)); - - like1 = Like.builder().user(user2).product(product1).build(); - like2 = Like.builder().user(user3).product(product1).build(); - like3 = Like.builder().user(user1).product(product3).build(); - likeRepository.saveAll(List.of(like1, like2, like3)); - - auction1 = Auction.toEntity(product6); - auctionRepository.save(auction1); - } - - @AfterEach - void tearDown() { - likeRepository.deleteAll(); - imageRepository.deleteAll(); - productRepository.deleteAll(); - userRepository.deleteAll(); - auctionRepository.deleteAll(); - } - - @Nested - @DisplayName("카테고리 별 사전 등록 상품 목록 조회 테스트") - class FindProductsByCategoryTest { - @Test - @DisplayName("1. 특정 카테고리 사전 등록 상품을 높은 가격순으로 조회") - public void testFindProductsByCategoryExpensive() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); - - // when - Page result = productRepository.findProductsByCategory(ELECTRONICS, user1.getId(), - pageable); - - // then - assertThat(result).isNotNull(); - assertThat(result.getContent()).hasSize(3); - assertThat(result.getContent()) - .extracting(ProductResponse::getMinPrice) - .isSortedAccordingTo(Comparator.reverseOrder()); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("사전등록상품4"); - assertThat(result.getContent().get(0).getMinPrice()).isEqualTo(40000); - assertThat(result.getContent().get(1).getProductName()).isEqualTo("사전등록상품3"); - assertThat(result.getContent().get(1).getMinPrice()).isEqualTo(30000); - assertThat(result.getContent().get(2).getProductName()).isEqualTo("사전등록상품1"); - assertThat(result.getContent().get(2).getMinPrice()).isEqualTo(10000); - } - - @Test - @DisplayName("2. 특정 카테고리 사전 등록 상품을 좋아요 많은 순으로 조회") - void findProductsByCategoryOrderByMostLikes() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("like")); - - // when - Page result = productRepository.findProductsByCategory(ELECTRONICS, user1.getId(), - pageable); - - // then - assertThat(result.getContent()).isNotEmpty(); - assertThat(result.getContent()).hasSize(3); - assertThat(result.getContent()) - .extracting(ProductResponse::getLikeCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("사전등록상품1"); - assertThat(result.getContent().get(0).getLikeCount()).isEqualTo(2); - assertThat(result.getContent().get(1).getProductName()).isEqualTo("사전등록상품3"); - assertThat(result.getContent().get(1).getLikeCount()).isEqualTo(1); - } - - @Test - @DisplayName("3. 특정 카테고리 사전 등록 상품 최신순으로 조회") - void findProductsByCategoryOrderByNewest() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("product-newest")); - - // when - Page result = productRepository.findProductsByCategory(ELECTRONICS, user1.getId(), - pageable); - - // then - assertThat(result.getContent()).isNotEmpty(); - assertThat(result.getContent()).hasSize(3); - assertThat(result.getContent()) - .extracting(ProductResponse::getProductId) - .isSortedAccordingTo(Comparator.reverseOrder()); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("사전등록상품4"); - assertThat(result.getContent().get(1).getProductName()).isEqualTo("사전등록상품3"); - assertThat(result.getContent().get(2).getProductName()).isEqualTo("사전등록상품1"); - - } - - @Test - @DisplayName("4. 카테고리가 null 일 때 모든 사전 등록 상품 조회") - void findProductsWithNullCategory() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("expensive")); - - // when - Page result = productRepository.findProductsByCategory(null, user1.getId(), pageable); - - // then - assertThat(result.getContent()).isNotNull(); - assertThat(result.getContent()).hasSizeGreaterThan(3); - assertThat(result.getContent()) - .extracting(ProductResponse::getMinPrice) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - } - - @Nested - @DisplayName("사전 등록 상품 상세 정보 조회 테스트") - class FindProductDetailsTest { - - @Test - @DisplayName("1. 유효한 상품 ID로 상세 정보 조회") - void findProductDetailsById() { - // when - Optional result = productRepository.findProductDetailsById(product1.getId(), - user2.getId()); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getProductName()).isEqualTo("사전등록상품1"); - assertThat(result.get().getMinPrice()).isEqualTo(10000); - assertThat(result.get().getLikeCount()).isEqualTo(2); - assertThat(result.get().getIsLiked()).isTrue(); // user2가 좋아요 한 상품 - assertThat(result.get().getIsSeller()).isFalse(); - assertThat(result.get().getCategory()).isEqualTo(ELECTRONICS); - - assertThat(result.get().getImages()).isNotEmpty(); - assertThat(result.get().getImages()).anySatisfy(imageResponse -> { - assertThat(imageResponse.imageUrl()).isEqualTo("path/to/image1.jpg"); - }); - } - - @Test - @DisplayName("2. 존재하지 않는 상품 ID로 조회 시 빈 Optional 반환") - void findProductDetailsByNonExistentId() { - // when - Optional result = productRepository.findProductDetailsById(999L, user1.getId()); - - // then - assertThat(result).isEmpty(); - } - - @Test - @DisplayName("3. 좋아요 하지 않은 사용자가 조회 시 'isLiked' false 반환") - void findProductDetailsWithoutLike() { - // when - Optional result = productRepository.findProductDetailsById(product3.getId(), - user3.getId()); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getIsLiked()).isFalse(); - } - - @Test - @DisplayName("4. 좋아요 없는 상품 조회 시 좋아요 수 0으로 반환") - void findProductDetailsWithNoLikes() { - // given - Product productWithoutLikes = productRepository.save( - Product.builder().user(user1).name("좋아요 없는 상품").category(ELECTRONICS).minPrice(10000).build() - ); - - // when - Optional result = productRepository.findProductDetailsById( - productWithoutLikes.getId(), user1.getId()); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getLikeCount()).isZero(); - } - - @Test - @DisplayName("5. 다른 사용자의 상품 정보도 정상적으로 조회") - void findProductDetailsOfOtherUser() { - // when - Optional result = productRepository.findProductDetailsById(product2.getId(), - user3.getId()); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getSellerNickname()).isEqualTo(user1.getNickname()); - } - - @Test - @DisplayName("6. 상품 정보에 생성 시간이 정확히 포함") - void findProductDetailsWithCorrectCreatedAt() { - // when - Optional result = productRepository.findProductDetailsById(product1.getId(), - user1.getId()); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getUpdatedAt()).isNotNull(); - // 생성 시간이 현재 시간보다 과거인지 확인 - assertThat(result.get().getUpdatedAt()).isBefore(LocalDateTime.now()); - } - - @Test - @DisplayName("7. 비안증 사용자의 요청인 경우") - void findProductDetailsWithAnonymousUser() { - // when - Optional result = productRepository.findProductDetailsById(product1.getId(), null); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getProductName()).isEqualTo("사전등록상품1"); - assertThat(result.get().getMinPrice()).isEqualTo(10000); - assertThat(result.get().getLikeCount()).isEqualTo(2); - assertThat(result.get().getIsLiked()).isFalse(); // user2가 좋아요 한 상품 - assertThat(result.get().getIsSeller()).isFalse(); - assertThat(result.get().getCategory()).isEqualTo(ELECTRONICS); - - // 이미지 확인 - assertThat(result.get().getImages()).isNotEmpty(); - assertThat(result.get().getImages()).anySatisfy(imageResponse -> { - assertThat(imageResponse.imageUrl()).isEqualTo("path/to/image1.jpg"); - }); - } - - @Test - @DisplayName("8. 정식 경매 상품의 사전 경매 조회 불가") - void findPreAuctionDetailsForRegularAuctionProduct() { - Optional result = productRepository.findProductDetailsById( - product6.getId(), null); - assertThat(result).isEmpty(); - } - - } - - @Nested - @DisplayName("나의 사전 등록 상품 목록 조회 테스트") - class FindMyProductsTest { - - @Test - @DisplayName("1. 유효한 사용자의 사전 등록 상품 목록 조회") - void findMyProductsByUserId() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by("product-newest")); - - // when - Page result = productRepository.findProductsByNickname(user1.getNickname(), pageable); - - // then - assertThat(result.getContent()).isNotEmpty(); - assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent()) - .extracting("productName") - .containsExactly("사전등록상품2", "사전등록상품1"); - } - - @Test - @DisplayName("2. 사전 등록 상품이 없는 사용자의 경우 빈 목록 반환") - void findMyProductsByUserIdWithNoProducts() { - // given - Pageable pageable = PageRequest.of(0, 10); - User newUser = userRepository.save( - User.builder().providerId("9999").nickname("새로운사용자").email("new@test.com").build()); - - // when - Page result = productRepository.findProductsByNickname(newUser.getNickname(), pageable); - - // then - assertThat(result.getContent()).isEmpty(); - } - - @Test - @DisplayName("3. 페이지네이션 동작 확인") - public void testPagination() { - // given - Pageable firstPage = PageRequest.of(0, 2, Sort.by("expensive")); - Pageable secondPage = PageRequest.of(1, 2, Sort.by("expensive")); - - // when - Page firstResult = productRepository.findProductsByCategory(ELECTRONICS, user1.getId(), - firstPage); - Page secondResult = productRepository.findProductsByCategory(ELECTRONICS, user1.getId(), - secondPage); - - // then - assertThat(firstResult.getContent()).hasSize(2); - assertThat(secondResult.getContent()).hasSize(1); - assertThat(firstResult.getContent().get(0).getProductName()).isEqualTo("사전등록상품4"); - assertThat(firstResult.getContent().get(1).getProductName()).isEqualTo("사전등록상품3"); - assertThat(secondResult.getContent().get(0).getProductName()).isEqualTo("사전등록상품1"); - } - - @Test - @DisplayName("4. 다양한 정렬 옵션이 정상적으로 적용") - void sortingOptionsWorkCheck() { - // given - Pageable newestPageable = PageRequest.of(0, 10, Sort.by("product-newest")); - Pageable expensivePageable = PageRequest.of(0, 10, Sort.by("expensive")); - - // when - Page newestResult = productRepository.findProductsByNickname(user1.getNickname(), - newestPageable); - Page expensiveResult = productRepository.findProductsByNickname(user1.getNickname(), - expensivePageable); - - // then - assertThat(newestResult.getContent()).extracting("productName") - .containsExactly("사전등록상품2", "사전등록상품1"); - assertThat(expensiveResult.getContent()).extracting("productName") - .containsExactly("사전등록상품2", "사전등록상품1"); - } - - @Test - @DisplayName("5. 조회된 상품에 좋아요 정보 포함 확인") - void likeInfoCheck() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - Page result = productRepository.findProductsByNickname(user1.getNickname(), pageable); - - // then - assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent()) - .extracting("likeCount") - .containsExactly(2L, 0L); - } - } - - @Nested - @DisplayName("사전 등록 상품 수정 테스트") - class UpdateProducts { - - @Test - @DisplayName("1. 상품 정보 수정 성공") - void updateProductInfo() { - // given - String newName = "수정된 상품명"; - String newDescription = "수정된 설명"; - Category newCategory = HOME_APPLIANCES; - int newMinPrice = 15000; - - // when - Product productToUpdate = productRepository.findById(product1.getId()).orElseThrow(); - productToUpdate.update(new UpdateProductRequest(newName, newDescription, newCategory, newMinPrice, null)); - productRepository.save(productToUpdate); - entityManager.flush(); - entityManager.clear(); - - // then - Product updatedProduct = productRepository.findById(product1.getId()).orElseThrow(); - assertThat(updatedProduct.getName()).isEqualTo(newName); - assertThat(updatedProduct.getDescription()).isEqualTo(newDescription); - assertThat(updatedProduct.getCategory()).isEqualTo(newCategory); - assertThat(updatedProduct.getMinPrice()).isEqualTo(newMinPrice); - } - - @Test - @DisplayName("2. 이미지 추가 성공") - void addNewImage() { - // given - String newImagePath = "path/to/new_image.jpg"; - Image newImage = Image.builder().product(product1).cdnPath(newImagePath).build(); - - // when - imageRepository.save(newImage); - entityManager.flush(); - entityManager.clear(); - - // then - Product updatedProduct = productRepository.findById(product1.getId()).orElseThrow(); - assertThat(updatedProduct.getImages()).hasSize(2); - assertThat(updatedProduct.getImages().stream().map(Image::getCdnPath)).contains(newImagePath); - } - - @Test - @DisplayName("3. 이미지 삭제 성공") - void deleteImage() { - // given - Long imageIdToDelete = image1.getId(); - - // when - imageRepository.deleteById(imageIdToDelete); - entityManager.flush(); - entityManager.clear(); - - // then - Product updatedProduct = productRepository.findById(product1.getId()).orElseThrow(); - assertThat(updatedProduct.getImages()).isEmpty(); - } - - @Test - @DisplayName("4. 상품 정보 수정 및 이미지 변경 성공") - void updateProductInfoAndChangeImages() { - // given - String newName = "수정된 상품명"; - String newImagePath = "path/to/new_image.jpg"; - Image newImage = Image.builder().product(product1).cdnPath(newImagePath).build(); - - // when - Product productToUpdate = productRepository.findById(product1.getId()).orElseThrow(); - productToUpdate.update(new UpdateProductRequest(newName, null, HOME_APPLIANCES, null, null)); - imageRepository.deleteById(image1.getId()); - imageRepository.save(newImage); - productRepository.save(productToUpdate); - entityManager.flush(); - entityManager.clear(); - - // then - Product updatedProduct = productRepository.findById(product1.getId()).orElseThrow(); - assertThat(updatedProduct.getName()).isEqualTo(newName); - assertThat(updatedProduct.getImages()).hasSize(1); - assertThat(updatedProduct.getImages().get(0).getCdnPath()).isEqualTo(newImagePath); - } - - @Test - @DisplayName("5. 존재하지 않는 상품 수정 시도") - void updateNonExistentProduct() { - // given - Long nonExistentProductId = 9999L; - - // when & then - assertThatThrownBy(() -> { - Product nonExistentProduct = productRepository.findById(nonExistentProductId).orElseThrow(); - nonExistentProduct.update(new UpdateProductRequest("New Name", null, null, null, null)); - }).isInstanceOf(NoSuchElementException.class); - } - - } -} diff --git a/src/test/java/org/chzz/market/domain/product/service/ProductServiceTest.java b/src/test/java/org/chzz/market/domain/product/service/ProductServiceTest.java deleted file mode 100644 index 39dffb93..00000000 --- a/src/test/java/org/chzz/market/domain/product/service/ProductServiceTest.java +++ /dev/null @@ -1,517 +0,0 @@ -package org.chzz.market.domain.product.service; - - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.chzz.market.domain.product.entity.Product.Category.ELECTRONICS; -import static org.chzz.market.domain.product.entity.Product.Category.HOME_APPLIANCES; -import static org.chzz.market.domain.product.error.ProductErrorCode.ALREADY_IN_AUCTION; -import static org.chzz.market.domain.product.error.ProductErrorCode.FORBIDDEN_PRODUCT_ACCESS; -import static org.chzz.market.domain.product.error.ProductErrorCode.PRODUCT_ALREADY_AUCTIONED; -import static org.chzz.market.domain.product.error.ProductErrorCode.PRODUCT_NOT_FOUND; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.product.dto.DeleteProductResponse; -import org.chzz.market.domain.product.dto.ProductResponse; -import org.chzz.market.domain.product.dto.UpdateProductRequest; -import org.chzz.market.domain.product.dto.UpdateProductResponse; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.product.error.ProductException; -import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.entity.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -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.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -@ExtendWith(MockitoExtension.class) -public class ProductServiceTest { - @Mock - private AuctionRepository auctionRepository; - - @Mock - private ProductRepository productRepository; - - @Mock - private ImageService imageService; - - @InjectMocks - private ProductService productService; - - private UpdateProductRequest updateRequest, updateRequest2, updateRequest3; - private Product existingProduct, existingProduct2; - private Image image; - private Product product; - private User user; - - @BeforeEach - void setUp() { - user = User.builder() - .id(1L) - .email("test@naver.com") - .nickname("테스트 유저") - .build(); - - image = Image.builder() - .product(existingProduct) - .id(1L) - .cdnPath("path/to/image.jpg") - .build(); - - product = Product.builder() - .id(1L) - .user(user) - .name("사전 등록 상품") - .description("사전 등록 상품 설명") - .category(ELECTRONICS) - .minPrice(10000) - .images(new ArrayList<>()) - .build(); - - existingProduct = Product.builder() - .id(1L) - .user(user) - .name("기존 상품") - .description("기존 설명") - .category(ELECTRONICS) - .minPrice(10000) - .images(new ArrayList<>()) - .build(); - - existingProduct2 = Product.builder() - .id(1L) - .user(user) - .name("기존 상품") - .description("기존 설명") - .category(ELECTRONICS) - .minPrice(10000) - .images(new ArrayList<>(List.of(image))) - .build(); - - updateRequest = UpdateProductRequest.builder() - .productName("수정된 상품") - .description("수정된 설명") - .category(HOME_APPLIANCES) - .minPrice(20000) - .imageSequence(Map.of(1L, 1, 2L, 2)) - .build(); - - updateRequest2 = UpdateProductRequest.builder() - .productName("수정된 상품") - .description("수정된 설명") - .category(HOME_APPLIANCES) - .minPrice(20000) - .imageSequence(Map.of(1L, 1, 2L, 2)) - .build(); - - updateRequest3 = UpdateProductRequest.builder() - .productName("수정된 상품") - .description("수정된 설명") - .category(HOME_APPLIANCES) - .minPrice(20000) - .imageSequence(Collections.emptyMap()) - .build(); - - System.setProperty("org.mockito.logging.verbosity", "all"); - } - - @Nested - @DisplayName("사전 등록 상품 수정") - class preRegister_Update { - - @Test - @DisplayName("1. 유효한 요청으로 사전 등록 상품 수정 성공 응답") - void updateProduct_Success() { - // given - Map newImages = createMockMultipartFiles(); - List existingImages = createExistingImages(); - existingProduct.addImages(existingImages); - - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn(Optional.of(existingProduct)); - - // when - UpdateProductResponse response = productService.updateProduct( - user.getId(), - 1L, - updateRequest, - newImages - ); - - // then - assertThat(response).isNotNull(); - assertThat(response.productName()).isEqualTo("수정된 상품"); - assertThat(response.description()).isEqualTo("수정된 설명"); - assertThat(response.category()).isEqualTo(HOME_APPLIANCES); - assertThat(response.minPrice()).isEqualTo(20000); - assertEquals(2, response.imageUrls().size()); - - // 기존 이미지와 변경 x - assertThat(response.imageUrls().get(0).imageUrl()).isEqualTo("existingImage1.jpg"); - assertThat(response.imageUrls().get(0).imageId()).isEqualTo(1L); - assertThat(response.imageUrls().get(1).imageUrl()).isEqualTo("existingImage2.jpg"); - assertThat(response.imageUrls().get(1).imageId()).isEqualTo(2L); - } - - @Test - @DisplayName("2. 존재하지 않는 상품으로 수정 시도 실패") - void updateProduct_ProductNotFound() { - // given - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(user.getId(), 1L, updateRequest, null)) - .isInstanceOf(ProductException.class) - .hasMessageContaining(PRODUCT_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("3. 이미 경매 등록된 상품 수정 시도 실패") - void updateProduct_AlreadyInAuction() { - // given - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn(Optional.of(existingProduct)); - when(auctionRepository.existsByProductId(anyLong())).thenReturn(true); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(user.getId(), 1L, updateRequest, null)) - .isInstanceOf(ProductException.class) - .hasMessageContaining(ALREADY_IN_AUCTION.getMessage()); - } - - @Test - @DisplayName("4. 이미지 수정 없이 상품 정보만 수정 성공") - void updateProduct_WithoutImages() { - // given - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn( - Optional.of(existingProduct2)); - when(auctionRepository.existsByProductId(anyLong())).thenReturn(false); - - // when - UpdateProductResponse response = productService.updateProduct( - user.getId(), - 1L, - updateRequest2, - new HashMap<>()); - - // then - assertThat(response).isNotNull(); - assertThat(response.productName()).isEqualTo("수정된 상품"); - assertThat(response.description()).isEqualTo("수정된 설명"); - assertThat(response.category()).isEqualTo(HOME_APPLIANCES); - assertThat(response.minPrice()).isEqualTo(20000); - assertEquals(1, response.imageUrls().size()); - } - - @Test - @DisplayName("5. 유효하지 않은 사용자가 상품 수정 시도 실패") - void updateProduct_InvalidUser() { - // given - UpdateProductRequest invalidUserRequest = UpdateProductRequest.builder() - .productName("수정된 상품") - .description("수정된 설명") - .category(HOME_APPLIANCES) - .minPrice(20000) - .build(); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(999L, 1L, invalidUserRequest, null)) - .isInstanceOf(ProductException.class) - .hasMessageContaining(PRODUCT_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("6. 소유자 아닌 사용자가 상품 수정 시도 실패") - void updateProduct_InvalidOwner() { - // given - UpdateProductRequest invalidUserRequest = UpdateProductRequest.builder() - .productName("수정된 상품") - .description("수정된 설명") - .category(HOME_APPLIANCES) - .minPrice(20000) - .build(); - - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn( - Optional.of(existingProduct)); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(2L, 1L, invalidUserRequest, null)) - .isInstanceOf(ProductException.class) - .hasMessageContaining(FORBIDDEN_PRODUCT_ACCESS.getMessage()); - } - - @Test - @DisplayName("7. 이미지 삭제 시 새 이미지 추가 테스트") - void updateProduct_WithImageChanges() { - // given - Map newImages = createMockMultipartFiles(); - - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn(Optional.of(existingProduct)); - when(auctionRepository.existsByProductId(anyLong())).thenReturn(false); - - when(imageService.uploadSequentialImages(eq(existingProduct), anyMap())) - .thenReturn(List.of( - new Image(1L, "new_image1.jpg", 2, existingProduct), - new Image(2L, "new_image2.jpg", 1, existingProduct) - )); - - UpdateProductRequest updateRequest = UpdateProductRequest.builder() - .productName("수정된 상품") - .description("수정된 설명") - .category(HOME_APPLIANCES) - .minPrice(20000) - .imageSequence(Collections.emptyMap()) - .build(); - - // when - UpdateProductResponse response = productService.updateProduct( - user.getId(), - 1L, - updateRequest, - newImages - ); - - // then - assertEquals(2, response.imageUrls().size()); - verify(imageService).updateImageSequences(new ArrayList<>(), updateRequest.getImageSequence()); - - assertThat(response.imageUrls().get(0).imageUrl()).isEqualTo("new_image1.jpg"); - assertThat(response.imageUrls().get(0).imageId()).isEqualTo(1L); - assertThat(response.imageUrls().get(1).imageUrl()).isEqualTo("new_image2.jpg"); - assertThat(response.imageUrls().get(1).imageId()).isEqualTo(2L); - } - - @Test - @DisplayName("8. 권한이 없는 사용자의 상품 수정 시도 실패") - void updateProduct_Unauthorized() { - // given - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn(Optional.of(existingProduct)); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(2L, 1L, updateRequest, null)) - .isInstanceOf(ProductException.class) - .hasMessageContaining(FORBIDDEN_PRODUCT_ACCESS.getMessage()); - } - - @Test - @DisplayName("9. 모든 기존 이미지 삭제 후 새 이미지 한 개 추가") - void updateProduct_EmptyImageList() { - // given - Map newImages = createMockMultipartFiles(); - List existingImages = createExistingImages(); - existingProduct.addImages(existingImages); - - when(productRepository.findProductByIdWithImage(anyLong())).thenReturn(Optional.of(existingProduct)); - when(auctionRepository.existsByProductId(anyLong())).thenReturn(false); - - when(imageService.uploadSequentialImages(eq(existingProduct), anyMap())) - .thenReturn(List.of( - new Image(3L, "new_image1.jpg", 1, existingProduct) // 새로 추가될 이미지 - )); - - // when - UpdateProductResponse response = productService.updateProduct( - user.getId(), - 1L, - updateRequest3, - newImages // 새로운 이미지가 추가됨 - ); - - // 이미지가 하나만 존재해야 함 (기존 이미지는 모두 삭제되고 새로운 이미지만 추가됨) - assertEquals(1, response.imageUrls().size()); - assertThat(response.imageUrls().get(0).imageUrl()).isEqualTo("new_image1.jpg"); - assertThat(response.imageUrls().get(0).imageId()).isEqualTo(3L); - } - } - - @Nested - @DisplayName("상품 삭제 테스트") - class DeleteProductTest { - - @Test - @DisplayName("1. 유효한 요청으로 사전 상품 삭제 성공 응답") - void deletePreRegisteredProduct_Success() { - // given - when(productRepository.findById(anyLong())).thenReturn(Optional.of(product)); - when(auctionRepository.existsByProductId(anyLong())).thenReturn(false); - - // when - DeleteProductResponse response = productService.deleteProduct(1L, 1L); - - // then - assertThat(response.productId()).isEqualTo(1L); - assertThat(response.productName()).isEqualTo("사전 등록 상품"); - assertThat(response.likeCount()).isZero(); - } - - @Test - @DisplayName("2. 이미 경매로 등록된 상품 삭제 시도") - void deleteAlreadyAuctionedProduct() { - // Given - when(productRepository.findById(anyLong())).thenReturn(Optional.of(product)); - when(auctionRepository.existsByProductId(anyLong())).thenReturn(true); - - // When & Then - assertThatThrownBy(() -> productService.deleteProduct(1L, 1L)) - .isInstanceOf(ProductException.class) - .hasMessage(PRODUCT_ALREADY_AUCTIONED.getMessage()); - } - - @Test - @DisplayName("3. 존재하지 않는 상품 삭제 시도") - void deleteNonExistingProduct() { - // Given - when(productRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // When & Then - assertThatThrownBy(() -> productService.deleteProduct(1L, 1L)) - .isInstanceOf(ProductException.class) - .hasMessage(PRODUCT_NOT_FOUND.getMessage()); - } - } - - @Nested - @DisplayName("내가 참여한 사전경매 조회 테스트") - class GetLikedProductListTest { - @Test - @DisplayName("1. 유효한 요청으로 좋아요한 사전 경매 상품 목록 조회 성공") - void getLikedProductList_Success() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt")); - - List mockProducts = Arrays.asList( - new ProductResponse(1L, "Product 1", "image1.jpg", 10000, 5L, true), - new ProductResponse(2L, "Product 2", "image2.jpg", 20000, 10L, true) - ); - - Page mockPage = new PageImpl<>(mockProducts, pageable, mockProducts.size()); - - when(productRepository.findLikedProductsByUserId(userId, pageable)).thenReturn(mockPage); - - // when - Page result = productService.getLikedProductList(userId, pageable); - - // then - assertNotNull(result); - assertEquals(2, result.getContent().size()); - assertEquals("Product 1", result.getContent().get(0).getProductName()); - assertEquals("Product 2", result.getContent().get(1).getProductName()); - assertTrue(result.getContent().get(0).getIsLiked()); - assertTrue(result.getContent().get(1).getIsLiked()); - - verify(productRepository, times(1)).findLikedProductsByUserId(userId, pageable); - } - - @Test - @DisplayName("2. 좋아요 한 사전경매 상품이 없는 경우 빈 목록 반환") - void getLikedProductList_EmptyList() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt")); - - Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0); - when(productRepository.findLikedProductsByUserId(userId, pageable)).thenReturn(emptyPage); - - // when - Page result = productService.getLikedProductList(userId, pageable); - - // then - assertNotNull(result); - assertTrue(result.getContent().isEmpty()); - assertEquals(0, result.getTotalElements()); - - verify(productRepository, times(1)).findLikedProductsByUserId(userId, pageable); - } - - @Test - @DisplayName("3. 페이지네이션 동작 확인") - void getLikedProductList_Pagination() { - // given - Long userId = 1L; - Pageable firstPageable = PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createdAt")); - Pageable secondPageable = PageRequest.of(1, 1, Sort.by(Sort.Direction.DESC, "createdAt")); - - List allProducts = Arrays.asList( - new ProductResponse(1L, "Product 1", "image1.jpg", 10000, 5L, true), - new ProductResponse(2L, "Product 2", "image2.jpg", 20000, 3L, true) - ); - - Page firstPage = new PageImpl<>(allProducts.subList(0, 1), firstPageable, - allProducts.size()); - Page secondPage = new PageImpl<>(allProducts.subList(1, 2), secondPageable, - allProducts.size()); - - when(productRepository.findLikedProductsByUserId(userId, firstPageable)).thenReturn(firstPage); - when(productRepository.findLikedProductsByUserId(userId, secondPageable)).thenReturn(secondPage); - - // when - Page firstResult = productService.getLikedProductList(userId, firstPageable); - Page secondResult = productService.getLikedProductList(userId, secondPageable); - - // then - assertEquals(1, firstResult.getContent().size()); - assertEquals("Product 1", firstResult.getContent().get(0).getProductName()); - assertEquals(1, secondResult.getContent().size()); - assertEquals("Product 2", secondResult.getContent().get(0).getProductName()); - - verify(productRepository, times(1)).findLikedProductsByUserId(userId, firstPageable); - verify(productRepository, times(1)).findLikedProductsByUserId(userId, secondPageable); - } - } - - private Map createMockMultipartFiles() { - Map images = new HashMap<>(); - - MultipartFile mockFile1 = new MockMultipartFile( - "image1", - "image1.jpg", - "image/jpeg", - "test image content 1".getBytes() - ); - - MultipartFile mockFile2 = new MockMultipartFile( - "image2", - "image2.jpg", - "image/jpeg", - "test image content 2".getBytes() - ); - images.put("1", mockFile1); - images.put("2", mockFile2); - return images; - } - - private List createExistingImages() { - return List.of( - new Image(1L, "existingImage1.jpg", 1, existingProduct), - new Image(2L, "existingImage2.jpg", 2, existingProduct) - ); - } -} diff --git a/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java b/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java index 558f3195..cd8f1851 100644 --- a/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java +++ b/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java @@ -9,13 +9,8 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import java.time.LocalDateTime; import java.util.Optional; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.image.service.ImageService; -import org.chzz.market.domain.product.entity.Product; import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest; import org.chzz.market.domain.user.dto.request.UserCreateRequest; import org.chzz.market.domain.user.dto.response.NicknameAvailabilityResponse; @@ -45,10 +40,6 @@ class UserServiceTest { private UserService userService; private User user1, user2, user3; - private Product product1, product2, product3, product4, product5, product6; - private Product auctionProduct1, auctionProduct2; - private Auction auction1, auction2, auction3, auction4, auction5, auction6, auction7, auction8; - private Bid bid1, bid2, bid3, bid4, bid5, bid6; private UpdateUserProfileRequest updateUserProfileRequest; @@ -73,41 +64,6 @@ void setUp() { .profileImageUrl("https://test") .build()); - product1 = Product.builder().id(1L).name("제품1").user(user2).minPrice(1000).build(); - product2 = Product.builder().id(2L).name("제품2").user(user2).minPrice(2000).build(); - product3 = Product.builder().id(3L).name("제품3").user(user2).minPrice(3000).build(); - product4 = Product.builder().id(4L).name("제품4").user(user2).minPrice(4000).build(); - product5 = Product.builder().id(5L).name("제품5").user(user2).minPrice(5000).build(); - product6 = Product.builder().id(6L).name("제품6").user(user2).minPrice(6000).build(); - - auction1 = Auction.builder().id(1L).product(product1).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(1)).build(); - auction2 = Auction.builder().id(2L).product(product2).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(2)).build(); - auction3 = Auction.builder().id(3L).product(product3).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(3)).build(); - auction4 = Auction.builder().id(4L).product(product4).status(AuctionStatus.ENDED) - .endDateTime(LocalDateTime.now().minusDays(1)).winnerId(user1.getId()).build(); - auction5 = Auction.builder().id(5L).product(product5).status(AuctionStatus.ENDED) - .endDateTime(LocalDateTime.now().minusDays(2)).winnerId(user1.getId()).build(); - auction6 = Auction.builder().id(6L).product(product6).status(AuctionStatus.ENDED) - .endDateTime(LocalDateTime.now().minusDays(3)).winnerId(user2.getId()).build(); - - bid1 = Bid.builder().id(1L).auctionId(auction1.getId()).bidderId(user1.getId()).amount(1500L).build(); - bid2 = Bid.builder().id(2L).auctionId(auction2.getId()).bidderId(user1.getId()).amount(2500L).build(); - bid3 = Bid.builder().id(3L).auctionId(auction3.getId()).bidderId(user1.getId()).amount(3500L).build(); - bid4 = Bid.builder().id(4L).auctionId(auction4.getId()).bidderId(user1.getId()).amount(4500L).build(); - bid5 = Bid.builder().id(5L).auctionId(auction5.getId()).bidderId(user1.getId()).amount(5500L).build(); - bid6 = Bid.builder().id(6L).auctionId(auction6.getId()).bidderId(user1.getId()).amount(6500L).build(); - - auctionProduct1 = Product.builder().id(9L).name("경매상품1").user(user1).minPrice(9000).build(); - auctionProduct2 = Product.builder().id(10L).name("경매상품2").user(user1).minPrice(10000).build(); - - auction7 = Auction.builder().id(7L).product(auctionProduct1).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(4)).build(); - auction8 = Auction.builder().id(8L).product(auctionProduct2).status(AuctionStatus.PROCEEDING) - .endDateTime(LocalDateTime.now().plusDays(5)).build(); - updateUserProfileRequest = UpdateUserProfileRequest.builder() .nickname("수정된 닉네임") .bio("수정된 자기 소개") @@ -122,7 +78,7 @@ class CreateUserTest { public void createUser_Success() throws Exception { // given Long userId = 1L; - UserCreateRequest userCreateRequest = new UserCreateRequest("bidderNickname","bio"); + UserCreateRequest userCreateRequest = new UserCreateRequest("bidderNickname", "bio"); User user = User.builder() .email("test@gmail.com") .providerId("123456") diff --git a/src/test/java/org/chzz/market/util/AuctionTestFactory.java b/src/test/java/org/chzz/market/util/AuctionTestFactory.java deleted file mode 100644 index 759d81c8..00000000 --- a/src/test/java/org/chzz/market/util/AuctionTestFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.chzz.market.util; - -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.auction.entity.Auction; -import org.chzz.market.domain.auction.type.AuctionStatus; -import org.chzz.market.domain.product.entity.Product; -import org.springframework.test.util.ReflectionTestUtils; - -import java.lang.reflect.Constructor; -import java.time.LocalDateTime; - -public class AuctionTestFactory { - public static Auction createAuction(Product product, BaseRegisterRequest request, AuctionStatus status) { - try { - // 리플렉션을 사용하여 protected 생성자에 접근 - Constructor constructor = Auction.class.getDeclaredConstructor(); - constructor.setAccessible(true); - Auction auction = constructor.newInstance(); - - // 리플렉션을 사용하여 필드 값 설정 - ReflectionTestUtils.setField(auction, "product", product); - ReflectionTestUtils.setField(auction, "status", status); - ReflectionTestUtils.setField(auction, "endDateTime", LocalDateTime.now().plusHours(24)); - - return auction; - } catch (Exception e) { - throw new RuntimeException("테스트를 위한 경매 인스턴스 생성에 실패했습니다.", e); - } - } -} diff --git a/src/test/java/org/chzz/market/util/ProductTestFactory.java b/src/test/java/org/chzz/market/util/ProductTestFactory.java deleted file mode 100644 index 0a42c831..00000000 --- a/src/test/java/org/chzz/market/util/ProductTestFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.chzz.market.util; - -import org.chzz.market.domain.auction.dto.request.BaseRegisterRequest; -import org.chzz.market.domain.product.entity.Product; -import org.chzz.market.domain.user.entity.User; -import org.springframework.test.util.ReflectionTestUtils; - -import java.lang.reflect.Constructor; - -public class ProductTestFactory { - public static Product createProduct(BaseRegisterRequest request, User user) { - try { - Constructor constructor = Product.class.getDeclaredConstructor(); - constructor.setAccessible(true); - Product product = constructor.newInstance(); - - ReflectionTestUtils.setField(product, "user", user); - ReflectionTestUtils.setField(product, "name", request.getProductName()); - ReflectionTestUtils.setField(product, "description", request.getDescription()); - ReflectionTestUtils.setField(product, "category", request.getCategory()); - - return product; - } catch (Exception e) { - throw new RuntimeException("테스트를 위한 상품 인스턴스 생성에 실패했습니다.", e); - } - } -} From 499fc06e946e309724f8cd65d4470cfba0b6ae1d Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:42:10 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=EC=88=9C=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 누락된 좋아요순 정렬 추가 * refactor: 좋아요 정렬 이름 변경 * docs: 정렬 설명 추가 --- .../chzz/market/domain/auction/controller/AuctionApi.java | 7 ++++++- .../domain/auction/repository/AuctionQueryRepository.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java index 43bc7efd..1505bb26 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java @@ -45,7 +45,12 @@ @Tag(name = "auctions", description = "경매 API") @RequestMapping("/v1/auctions") public interface AuctionApi { - @Operation(summary = "경매 목록 조회", description = "경매 목록을 조회합니다. status 파라미터를 통해 조회 유형을 지정합니다.") + @Operation( + summary = "경매 목록 조회", + description = "경매 목록을 조회합니다. status 파라미터를 통해 조회 유형을 지정합니다. 정렬 기준 (sort 파라미터): " + + "popularity(인기순), likes(좋아요순), expensive(높은 가격순), cheap(낮은 가격순), " + + "immediately(즉시 종료순), newest(최신순)." + ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "정식 경매 응답(페이징)", content = {@Content( diff --git a/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java index 1f33bdd7..47186ebf 100644 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java @@ -544,6 +544,7 @@ private JPQLQuery getWinningBidAmount() { @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum AuctionOrder implements QuerydslOrder { POPULARITY("popularity", auction.bidCount.desc()), + LIKES("likes", auction.likeCount.desc()), EXPENSIVE("expensive", auction.minPrice.desc()), CHEAP("cheap", auction.minPrice.asc()), IMMEDIATELY("immediately", timeRemaining().asc()), From 91201e303b0392b007228cf92fe827fdc733e293 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:39:13 +0900 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A7=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=8B=9C=20=EC=84=A4=EB=AA=85=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market/domain/auction/dto/request/UpdateAuctionRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java index 4d25611f..8802c186 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java @@ -18,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor public class UpdateAuctionRequest { - public static final String DESCRIPTION_REGEX = "^(?:(?:[^\n]\n){0,10}[^\n]$)"; // 개행문자 10개를 제한 + public static final String DESCRIPTION_REGEX = "^(?:(?:[^\\n]*\\n){0,10}[^\\n]*$)"; // 개행문자 10개를 제한 @Size(min = 2, max = 30, message = "제목은 최소 2글자 이상 30자 이하여야 합니다") private String productName; From 1c7c9942ba589590e1b1671cc4cee319714c0dc1 Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:39:06 +0900 Subject: [PATCH 13/16] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A7=A4=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=A7=81=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chzz/market/domain/auction/schedule/AuctionEndJob.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java b/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java index 862d2293..de2cf7c8 100644 --- a/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java +++ b/src/main/java/org/chzz/market/domain/auction/schedule/AuctionEndJob.java @@ -1,18 +1,18 @@ package org.chzz.market.domain.auction.schedule; -import lombok.RequiredArgsConstructor; import org.chzz.market.domain.auction.service.AuctionEndService; import org.quartz.Job; import org.quartz.JobExecutionContext; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 경매 스케줄링 종료 작업 */ @Component -@RequiredArgsConstructor public class AuctionEndJob implements Job { - private final AuctionEndService auctionEndService; + @Autowired + private AuctionEndService auctionEndService; @Override public void execute(JobExecutionContext context) { From 4fa1e22e40e6455edbdfcae47520841a51de100f Mon Sep 17 00:00:00 2001 From: Jun Choi <121853214+junest66@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:07:33 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: user field not null 조건 수정 스크립트 추가 * feat: 회원탈퇴 에러 코드 추가 * feat: 현재 사용자가 입찰 진행중인 경매 갯수 조회 쿼리 추가 * feat: User 필드에 not null 제거 스크립트 추가 * feat: 회원탈퇴 API 추가 * feat: 회원탈퇴 서비스 추가 * feat: 회원탈퇴 이벤트 dto 추가 * feat: 회원탈퇴 유저 익명화 처리 함수 추가 * refactor: 로그아웃시 Redis에 토큰이 없어도 예외처리 삭제 * chore: RestClientConfig 설정 추가 * style: 패키지 이동 * chore: OAuth2ClientConfig 추가 * feat: OAuth2 RefreshToken을 저장하는 레포지토리 추가 * feat: CustomUserDetails에 providerId를 꺼내는 함수 추가 * feat: CustomOAuth2LoginAuthenticationProvider 클래스 추가 - refreshToken 저장 * refactor: CustomOAuth2LoginAuthenticationProvider 적용 및 security url 수정 * feat: 회원탈퇴 이벤트를 받는 이벤트 리스너 추가 * feat: 소셜로그인 서비스 인터페이스 추가 * feat: 소셜로그인 팩토리 서비스 추가 * feat: 카카오 토큰 재발행 응답 dto 추가 * feat: 카카오 연결끊기 응답 dto 추가 * feat: 네이버 토큰 재발행 응답 dto 추가 * feat: 네이버 연결끊기 응답 dto 추가 * refactor: 가독성 있게 switch 리팩토링 * feat: 카카오 소셜로그인 서비스 구현클래스 추가 * feat: 네이버 소셜로그인 서비스 구현클래스 추가 * test: 테스트 제거 * chore: test api key 등록 --- .../common/config/OAuth2ClientConfig.java | 16 +++ .../common/config/RestClientConfig.java | 13 +++ .../market/common/config/SecurityConfig.java | 25 ++--- .../repository/AuctionQueryRepository.java | 12 ++ .../dto/response/KaKaoResponse.java | 2 +- .../dto/response/KakaoTokenResponse.java | 4 + .../dto/response/KakaoUnlinkResponse.java | 4 + .../dto/response/NaverResponse.java | 2 +- .../dto/response/NaverTokenResponse.java | 4 + .../dto/response/NaverUnlinkResponse.java | 4 + .../dto/response/OAuth2Response.java | 2 +- .../Oauth2RefreshTokenRepository.java | 34 ++++++ .../service}/CustomFailureHandler.java | 2 +- ...stomOAuth2LoginAuthenticationProvider.java | 41 +++++++ .../service/CustomOAuth2UserService.java | 23 ++-- .../service}/CustomSuccessHandler.java | 2 +- .../service/KakaoSocialLoginService.java | 84 ++++++++++++++ .../service/NaverSocialLoginService.java | 103 ++++++++++++++++++ .../service/SocialLoginEventListener.java | 22 ++++ .../oauth2/service/SocialLoginService.java | 9 ++ .../service/SocialLoginServiceFactory.java | 19 ++++ .../domain/token/service/TokenService.java | 9 +- .../domain/user/controller/UserApi.java | 17 +++ .../user/controller/UserController.java | 14 +++ .../domain/user/dto/CustomUserDetails.java | 6 +- .../domain/user/dto/UserDeletedEvent.java | 6 + .../chzz/market/domain/user/entity/User.java | 17 ++- .../domain/user/error/UserErrorCode.java | 22 +++- .../user/service/UserDeleteService.java | 58 ++++++++++ src/main/resources/application-test.yml | 4 + .../db/migration/V2__modify_user_field.sql | 4 + .../token/service/TokenServiceTest.java | 15 +-- .../user/oauth2/CustomSuccessHandlerTest.java | 3 +- 33 files changed, 538 insertions(+), 64 deletions(-) create mode 100644 src/main/java/org/chzz/market/common/config/OAuth2ClientConfig.java create mode 100644 src/main/java/org/chzz/market/common/config/RestClientConfig.java rename src/main/java/org/chzz/market/domain/{user => oauth2}/dto/response/KaKaoResponse.java (92%) create mode 100644 src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoTokenResponse.java create mode 100644 src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoUnlinkResponse.java rename src/main/java/org/chzz/market/domain/{user => oauth2}/dto/response/NaverResponse.java (92%) create mode 100644 src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverTokenResponse.java create mode 100644 src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverUnlinkResponse.java rename src/main/java/org/chzz/market/domain/{user => oauth2}/dto/response/OAuth2Response.java (91%) create mode 100644 src/main/java/org/chzz/market/domain/oauth2/repository/Oauth2RefreshTokenRepository.java rename src/main/java/org/chzz/market/domain/{user/oauth2 => oauth2/service}/CustomFailureHandler.java (96%) create mode 100644 src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2LoginAuthenticationProvider.java rename src/main/java/org/chzz/market/domain/{user => oauth2}/service/CustomOAuth2UserService.java (75%) rename src/main/java/org/chzz/market/domain/{user/oauth2 => oauth2/service}/CustomSuccessHandler.java (98%) create mode 100644 src/main/java/org/chzz/market/domain/oauth2/service/KakaoSocialLoginService.java create mode 100644 src/main/java/org/chzz/market/domain/oauth2/service/NaverSocialLoginService.java create mode 100644 src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginEventListener.java create mode 100644 src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginService.java create mode 100644 src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginServiceFactory.java create mode 100644 src/main/java/org/chzz/market/domain/user/dto/UserDeletedEvent.java create mode 100644 src/main/java/org/chzz/market/domain/user/service/UserDeleteService.java create mode 100644 src/main/resources/db/migration/V2__modify_user_field.sql diff --git a/src/main/java/org/chzz/market/common/config/OAuth2ClientConfig.java b/src/main/java/org/chzz/market/common/config/OAuth2ClientConfig.java new file mode 100644 index 00000000..bca0b1be --- /dev/null +++ b/src/main/java/org/chzz/market/common/config/OAuth2ClientConfig.java @@ -0,0 +1,16 @@ +package org.chzz.market.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; + +@Configuration +public class OAuth2ClientConfig { + + @Bean + public OAuth2AccessTokenResponseClient accessTokenResponseClient() { + return new DefaultAuthorizationCodeTokenResponseClient(); + } +} diff --git a/src/main/java/org/chzz/market/common/config/RestClientConfig.java b/src/main/java/org/chzz/market/common/config/RestClientConfig.java new file mode 100644 index 00000000..844da7ee --- /dev/null +++ b/src/main/java/org/chzz/market/common/config/RestClientConfig.java @@ -0,0 +1,13 @@ +package org.chzz.market.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + @Bean + RestClient restClient() { + return RestClient.create(); + } +} diff --git a/src/main/java/org/chzz/market/common/config/SecurityConfig.java b/src/main/java/org/chzz/market/common/config/SecurityConfig.java index c6e0e8b9..1319fa1c 100644 --- a/src/main/java/org/chzz/market/common/config/SecurityConfig.java +++ b/src/main/java/org/chzz/market/common/config/SecurityConfig.java @@ -14,9 +14,10 @@ import org.chzz.market.common.filter.JWTFilter; import org.chzz.market.common.filter.NotFoundFilter; import org.chzz.market.common.util.JWTUtil; -import org.chzz.market.domain.user.oauth2.CustomFailureHandler; -import org.chzz.market.domain.user.oauth2.CustomSuccessHandler; -import org.chzz.market.domain.user.service.CustomOAuth2UserService; +import org.chzz.market.domain.oauth2.service.CustomFailureHandler; +import org.chzz.market.domain.oauth2.service.CustomOAuth2LoginAuthenticationProvider; +import org.chzz.market.domain.oauth2.service.CustomOAuth2UserService; +import org.chzz.market.domain.oauth2.service.CustomSuccessHandler; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,6 +43,7 @@ public class SecurityConfig { @Value("${client.url}") private String clientUrl; private final CustomOAuth2UserService customOAuth2UserService; + private final CustomOAuth2LoginAuthenticationProvider customOAuth2LoginAuthenticationProvider; private final CustomAccessDeniedHandler customAccessDeniedHandler; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomSuccessHandler customSuccessHandler; @@ -53,28 +55,19 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception { - return http.authorizeHttpRequests(authorize -> authorize + return http + .authenticationProvider(customOAuth2LoginAuthenticationProvider) + .authorizeHttpRequests(authorize -> authorize .requestMatchers(ACTUATOR).permitAll() .requestMatchers("/metrics").permitAll() .requestMatchers("/api-docs", "/swagger-ui/**", "/api/v3/api-docs/**").permitAll() .requestMatchers(GET, "/api/v1/auctions", "/api/v1/auctions/{auctionId:\\d+}", - "/api/v1/auctions/{auctionId:\\d+}/simple", - "/api/v1/auctions/best", - "/api/v1/auctions/imminent", - "/api/v1/auctions/users/*", - "/api/v1/products", - "/api/v1/products/categories", - "/api/v1/products/{productId:\\d+}", - "/api/v1/products/users/*", + "/api/v1/auctions/categories", "/api/v1/notifications/subscribe", "/api/v1/users/*", "/api/v1/users/check/nickname/*").permitAll() - .requestMatchers(GET, - "/api/v2/auctions", - "/api/v2/auctions/categories", - "/api/v2/auctions/{auctionId:\\d+}").permitAll() .requestMatchers(POST, "/api/v1/users/tokens/reissue").permitAll() .requestMatchers(POST, "/api/v1/users").hasRole("TEMP_USER") diff --git a/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java index 47186ebf..7b625941 100644 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java +++ b/src/main/java/org/chzz/market/domain/auction/repository/AuctionQueryRepository.java @@ -489,6 +489,18 @@ public ParticipationCountsResponse getParticipationCounts(Long userId) { ); } + /** + * 현재 사용자가 입찰 진행 중인 경매 갯수 조회 + */ + public long countProceedingAuctionsByUserId(Long userId) { + return jpaQueryFactory + .select(auction.count()) + .from(auction) + .join(bid).on(bid.auctionId.eq(auction.id)).on(bid.bidderId.eq(userId).and(bid.status.eq(ACTIVE))) + .where(auction.status.eq(PROCEEDING)) + .fetchOne(); + } + private List getImagesByAuctionId(Long auctionId) { return jpaQueryFactory .select(new QImageResponse(image.id, image.cdnPath)) diff --git a/src/main/java/org/chzz/market/domain/user/dto/response/KaKaoResponse.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/KaKaoResponse.java similarity index 92% rename from src/main/java/org/chzz/market/domain/user/dto/response/KaKaoResponse.java rename to src/main/java/org/chzz/market/domain/oauth2/dto/response/KaKaoResponse.java index c894c002..fc43632f 100644 --- a/src/main/java/org/chzz/market/domain/user/dto/response/KaKaoResponse.java +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/KaKaoResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.user.dto.response; +package org.chzz.market.domain.oauth2.dto.response; import java.util.Map; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoTokenResponse.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoTokenResponse.java new file mode 100644 index 00000000..96d432fe --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoTokenResponse.java @@ -0,0 +1,4 @@ +package org.chzz.market.domain.oauth2.dto.response; + +public record KakaoTokenResponse(String token_type, String access_token, Integer expires_in) { +} diff --git a/src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoUnlinkResponse.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoUnlinkResponse.java new file mode 100644 index 00000000..037c8ab2 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/KakaoUnlinkResponse.java @@ -0,0 +1,4 @@ +package org.chzz.market.domain.oauth2.dto.response; + +public record KakaoUnlinkResponse(Long id) { +} diff --git a/src/main/java/org/chzz/market/domain/user/dto/response/NaverResponse.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverResponse.java similarity index 92% rename from src/main/java/org/chzz/market/domain/user/dto/response/NaverResponse.java rename to src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverResponse.java index 500a07ea..0b9de93d 100644 --- a/src/main/java/org/chzz/market/domain/user/dto/response/NaverResponse.java +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverResponse.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.user.dto.response; +package org.chzz.market.domain.oauth2.dto.response; import java.util.Map; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverTokenResponse.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverTokenResponse.java new file mode 100644 index 00000000..e0a4441d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverTokenResponse.java @@ -0,0 +1,4 @@ +package org.chzz.market.domain.oauth2.dto.response; + +public record NaverTokenResponse(String access_token, String token_type, String expires_in) { +} diff --git a/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverUnlinkResponse.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverUnlinkResponse.java new file mode 100644 index 00000000..2a1f994a --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/NaverUnlinkResponse.java @@ -0,0 +1,4 @@ +package org.chzz.market.domain.oauth2.dto.response; + +public record NaverUnlinkResponse(String access_token, String result) { +} diff --git a/src/main/java/org/chzz/market/domain/user/dto/response/OAuth2Response.java b/src/main/java/org/chzz/market/domain/oauth2/dto/response/OAuth2Response.java similarity index 91% rename from src/main/java/org/chzz/market/domain/user/dto/response/OAuth2Response.java rename to src/main/java/org/chzz/market/domain/oauth2/dto/response/OAuth2Response.java index e0fcf61e..975d3122 100644 --- a/src/main/java/org/chzz/market/domain/user/dto/response/OAuth2Response.java +++ b/src/main/java/org/chzz/market/domain/oauth2/dto/response/OAuth2Response.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.user.dto.response; +package org.chzz.market.domain.oauth2.dto.response; import static org.chzz.market.domain.user.entity.User.UserRole.TEMP_USER; diff --git a/src/main/java/org/chzz/market/domain/oauth2/repository/Oauth2RefreshTokenRepository.java b/src/main/java/org/chzz/market/domain/oauth2/repository/Oauth2RefreshTokenRepository.java new file mode 100644 index 00000000..d860f41d --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/repository/Oauth2RefreshTokenRepository.java @@ -0,0 +1,34 @@ +package org.chzz.market.domain.oauth2.repository; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.token.entity.TokenType; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class Oauth2RefreshTokenRepository { + private final RedisTemplate redisTemplate; + + public void save(String providerType, String providerId, String refreshToken) { + String key = generateKey(providerType, providerId); + redisTemplate.opsForValue().set(key, refreshToken); + redisTemplate.expire(key, TokenType.REFRESH.getExpirationTime(), TimeUnit.SECONDS); + } + + public Optional find(String providerType, String providerId) { + String key = generateKey(providerType, providerId); + return Optional.ofNullable(redisTemplate.opsForValue().get(key)); + } + + public void delete(String providerType, String providerId) { + String key = generateKey(providerType, providerId); + redisTemplate.delete(key); + } + + private String generateKey(String providerType, String providerId) { + return providerType + ":" + providerId; + } +} diff --git a/src/main/java/org/chzz/market/domain/user/oauth2/CustomFailureHandler.java b/src/main/java/org/chzz/market/domain/oauth2/service/CustomFailureHandler.java similarity index 96% rename from src/main/java/org/chzz/market/domain/user/oauth2/CustomFailureHandler.java rename to src/main/java/org/chzz/market/domain/oauth2/service/CustomFailureHandler.java index 698e5567..034982ec 100644 --- a/src/main/java/org/chzz/market/domain/user/oauth2/CustomFailureHandler.java +++ b/src/main/java/org/chzz/market/domain/oauth2/service/CustomFailureHandler.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.user.oauth2; +package org.chzz.market.domain.oauth2.service; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2LoginAuthenticationProvider.java b/src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2LoginAuthenticationProvider.java new file mode 100644 index 00000000..391a0772 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2LoginAuthenticationProvider.java @@ -0,0 +1,41 @@ +package org.chzz.market.domain.oauth2.service; + +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.user.dto.CustomUserDetails; +import org.chzz.market.domain.oauth2.repository.Oauth2RefreshTokenRepository; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class CustomOAuth2LoginAuthenticationProvider extends OAuth2LoginAuthenticationProvider { + private final Oauth2RefreshTokenRepository oauth2RefreshTokenRepository; + + public CustomOAuth2LoginAuthenticationProvider( + OAuth2AccessTokenResponseClient accessTokenResponseClient, + OAuth2UserService userService, + Oauth2RefreshTokenRepository oauth2RefreshTokenRepository) { + super(accessTokenResponseClient, userService); + this.oauth2RefreshTokenRepository = oauth2RefreshTokenRepository; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) super.authenticate( + authentication); + CustomUserDetails userDetails = (CustomUserDetails) authenticationResult.getPrincipal(); + String refreshToken = authenticationResult.getRefreshToken().getTokenValue(); + String providerType = authenticationResult.getClientRegistration().getRegistrationId(); + String providerId = userDetails.getProviderId(); + oauth2RefreshTokenRepository.save(providerType, providerId, refreshToken); + return authenticationResult; + } +} diff --git a/src/main/java/org/chzz/market/domain/user/service/CustomOAuth2UserService.java b/src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2UserService.java similarity index 75% rename from src/main/java/org/chzz/market/domain/user/service/CustomOAuth2UserService.java rename to src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2UserService.java index 02dadc7a..5fe993a2 100644 --- a/src/main/java/org/chzz/market/domain/user/service/CustomOAuth2UserService.java +++ b/src/main/java/org/chzz/market/domain/oauth2/service/CustomOAuth2UserService.java @@ -1,11 +1,11 @@ -package org.chzz.market.domain.user.service; +package org.chzz.market.domain.oauth2.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.oauth2.dto.response.KaKaoResponse; +import org.chzz.market.domain.oauth2.dto.response.NaverResponse; +import org.chzz.market.domain.oauth2.dto.response.OAuth2Response; import org.chzz.market.domain.user.dto.CustomUserDetails; -import org.chzz.market.domain.user.dto.response.KaKaoResponse; -import org.chzz.market.domain.user.dto.response.NaverResponse; -import org.chzz.market.domain.user.dto.response.OAuth2Response; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.entity.User.ProviderType; import org.chzz.market.domain.user.repository.UserRepository; @@ -34,17 +34,10 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic if (registrationId == null) { throw new OAuth2AuthenticationException("유효하지않는 OAuth2 제공자입니다."); } - OAuth2Response oAuth2Response; - switch (providerType) { - case NAVER: - oAuth2Response = new NaverResponse(oAuth2User.getAttributes()); - break; - case KAKAO: - oAuth2Response = new KaKaoResponse(oAuth2User.getAttributes()); - break; - default: - throw new OAuth2AuthenticationException("지원되지 않는 OAuth2 제공자입니다."); - } + OAuth2Response oAuth2Response = switch (providerType) { + case NAVER -> new NaverResponse(oAuth2User.getAttributes()); + case KAKAO -> new KaKaoResponse(oAuth2User.getAttributes()); + }; User user = findOrCreateMember(oAuth2Response, providerType); return new CustomUserDetails(user, oAuth2User.getAttributes()); } diff --git a/src/main/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandler.java b/src/main/java/org/chzz/market/domain/oauth2/service/CustomSuccessHandler.java similarity index 98% rename from src/main/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandler.java rename to src/main/java/org/chzz/market/domain/oauth2/service/CustomSuccessHandler.java index c5176bc9..89be89ca 100644 --- a/src/main/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandler.java +++ b/src/main/java/org/chzz/market/domain/oauth2/service/CustomSuccessHandler.java @@ -1,4 +1,4 @@ -package org.chzz.market.domain.user.oauth2; +package org.chzz.market.domain.oauth2.service; import static org.chzz.market.common.util.CookieUtil.createTokenCookie; diff --git a/src/main/java/org/chzz/market/domain/oauth2/service/KakaoSocialLoginService.java b/src/main/java/org/chzz/market/domain/oauth2/service/KakaoSocialLoginService.java new file mode 100644 index 00000000..b9dd8192 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/service/KakaoSocialLoginService.java @@ -0,0 +1,84 @@ +package org.chzz.market.domain.oauth2.service; + +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.oauth2.dto.response.KakaoTokenResponse; +import org.chzz.market.domain.oauth2.dto.response.KakaoUnlinkResponse; +import org.chzz.market.domain.oauth2.repository.Oauth2RefreshTokenRepository; +import org.chzz.market.domain.user.entity.User.ProviderType; +import org.chzz.market.domain.user.error.UserErrorCode; +import org.chzz.market.domain.user.error.exception.UserException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; + +@Service +@RequiredArgsConstructor +@Slf4j +public class KakaoSocialLoginService implements SocialLoginService { + private static final String KAKAO_UNLINK_URL = "https://kapi.kakao.com/v1/user/unlink"; + private static final String CONTENT_TYPE = APPLICATION_FORM_URLENCODED_VALUE + ";charset=utf-8"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_TOKEN_PREFIX = "Bearer "; + + private final RestClient restClient; + private final Oauth2RefreshTokenRepository oauth2RefreshTokenRepository; + + @Value("${spring.security.oauth2.client.provider.kakao.token-uri}") + private String kakaoTokenUrl; + + @Value("${oauth2.kakao.rest-api-key}") + private String restApiKey; + + @Value("${spring.security.oauth2.client.registration.kakao.client-secret}") + private String clientSecret; + + @Override + public void disconnect(ProviderType providerType, String providerId) { + String accessToken = getAccessToken(providerType, providerId); + KakaoUnlinkResponse kakaoUnlinkResponse = restClient.post() + .uri(KAKAO_UNLINK_URL) + .header(AUTHORIZATION_HEADER, BEARER_TOKEN_PREFIX + accessToken) + .contentType(MediaType.valueOf(CONTENT_TYPE)) + .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + log.error("카카오 연결끊기에 실패하였습니다. response status={}", response.getStatusCode()); + throw new UserException(UserErrorCode.KAKAO_UNLINK_FAILED); + }) + .body(KakaoUnlinkResponse.class); + log.info("카카오 providerId {} 의 연결이 성공적으로 끊어졌습니다.", kakaoUnlinkResponse.id()); + oauth2RefreshTokenRepository.delete(providerType.getName(), providerId); + } + + @Override + public String getAccessToken(ProviderType providerType, String providerId) { + String refreshToken = oauth2RefreshTokenRepository.find(providerType.getName(), providerId) + .orElseThrow(() -> new UserException(UserErrorCode.KAKAO_UNLINK_FAILED)); + KakaoTokenResponse kakaoTokenResponse = restClient.post() + .uri(kakaoTokenUrl) + .contentType(MediaType.valueOf(CONTENT_TYPE)) + .body(createRefreshTokenRequestBody(refreshToken)) + .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + log.error("Access 토큰 발급에 실패하였습니다. 응답 상태: {}", response.getStatusCode()); + throw new UserException(UserErrorCode.KAKAO_UNLINK_FAILED); + }) + .body(KakaoTokenResponse.class); + return kakaoTokenResponse.access_token(); + } + + private MultiValueMap createRefreshTokenRequestBody(String refreshToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("client_id", restApiKey); + body.add("refresh_token", refreshToken); + body.add("client_secret", clientSecret); + return body; + } +} diff --git a/src/main/java/org/chzz/market/domain/oauth2/service/NaverSocialLoginService.java b/src/main/java/org/chzz/market/domain/oauth2/service/NaverSocialLoginService.java new file mode 100644 index 00000000..670e1f92 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/service/NaverSocialLoginService.java @@ -0,0 +1,103 @@ +package org.chzz.market.domain.oauth2.service; + +import java.net.URI; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.chzz.market.domain.oauth2.dto.response.NaverTokenResponse; +import org.chzz.market.domain.oauth2.dto.response.NaverUnlinkResponse; +import org.chzz.market.domain.oauth2.repository.Oauth2RefreshTokenRepository; +import org.chzz.market.domain.user.entity.User.ProviderType; +import org.chzz.market.domain.user.error.UserErrorCode; +import org.chzz.market.domain.user.error.exception.UserException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NaverSocialLoginService implements SocialLoginService { + private static final String GRANT_TYPE_DELETE = "delete"; + private static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; + private static final String CLIENT_ID_PARAM = "client_id"; + private static final String CLIENT_SECRET_PARAM = "client_secret"; + private static final String ACCESS_TOKEN_PARAM = "access_token"; + private static final String REFRESH_TOKEN_PARAM = "refresh_token"; + private static final String GRANT_TYPE_PARAM = "grant_type"; + + private final RestClient restClient; + private final Oauth2RefreshTokenRepository oauth2RefreshTokenRepository; + + @Value("${spring.security.oauth2.client.provider.naver.token-uri}") + private String naverTokenUrl; + + @Value("${spring.security.oauth2.client.registration.naver.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.naver.client-secret}") + private String clientSecret; + + @Override + public void disconnect(ProviderType providerType, String providerId) { + String accessToken = getAccessToken(providerType, providerId); + + URI uri = UriComponentsBuilder.fromUriString(naverTokenUrl) + .queryParams(createDisconnectParams(accessToken)) + .build() + .toUri(); + + NaverUnlinkResponse naverUnlinkResponse = restClient.get() + .uri(uri) + .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + log.error("네이버 연결 끊기에 실패하였습니다. 응답 상태: {}", response.getStatusCode()); + throw new UserException(UserErrorCode.NAVER_UNLINK_FAILED); + }) + .body(NaverUnlinkResponse.class); + log.info("네이버 providerId {} 의 연결이 성공적으로 끊어졌습니다. 결과: {}", providerId, naverUnlinkResponse.result()); + oauth2RefreshTokenRepository.delete(providerType.getName(), providerId); + } + + private MultiValueMap createDisconnectParams(String accessToken) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(CLIENT_ID_PARAM, clientId); + params.add(CLIENT_SECRET_PARAM, clientSecret); + params.add(ACCESS_TOKEN_PARAM, accessToken); + params.add(GRANT_TYPE_PARAM, GRANT_TYPE_DELETE); + return params; + } + + @Override + public String getAccessToken(ProviderType providerType, String providerId) { + String refreshToken = oauth2RefreshTokenRepository.find(providerType.getName(), providerId) + .orElseThrow(() -> new UserException(UserErrorCode.NAVER_UNLINK_FAILED)); + + URI uri = UriComponentsBuilder.fromUriString(naverTokenUrl) + .queryParams(createRefreshTokenParams(refreshToken)) + .build() + .toUri(); + + NaverTokenResponse naverTokenResponse = restClient.get() + .uri(uri) + .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + log.error("네이버 액세스 토큰 발급에 실패하였습니다. 응답 상태: {}", response.getStatusCode()); + throw new UserException(UserErrorCode.NAVER_UNLINK_FAILED); + }) + .body(NaverTokenResponse.class); + return naverTokenResponse.access_token(); + } + + private MultiValueMap createRefreshTokenParams(String refreshToken) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(CLIENT_ID_PARAM, clientId); + params.add(CLIENT_SECRET_PARAM, clientSecret); + params.add(REFRESH_TOKEN_PARAM, refreshToken); + params.add(GRANT_TYPE_PARAM, GRANT_TYPE_REFRESH_TOKEN); + return params; + } +} diff --git a/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginEventListener.java b/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginEventListener.java new file mode 100644 index 00000000..bcd73f6a --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginEventListener.java @@ -0,0 +1,22 @@ +package org.chzz.market.domain.oauth2.service; + +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.user.dto.UserDeletedEvent; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class SocialLoginEventListener { + private final SocialLoginServiceFactory socialLoginServiceFactory; + + @Async("threadPoolTaskExecutor") + @TransactionalEventListener(phase = AFTER_COMMIT) + public void handleUserDeletedEvent(UserDeletedEvent event) { + SocialLoginService socialLoginService = socialLoginServiceFactory.getService(event.type()); + socialLoginService.disconnect(event.type(), event.providerId()); + } +} diff --git a/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginService.java b/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginService.java new file mode 100644 index 00000000..9016e0da --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginService.java @@ -0,0 +1,9 @@ +package org.chzz.market.domain.oauth2.service; + +import org.chzz.market.domain.user.entity.User.ProviderType; + +public interface SocialLoginService { + String getAccessToken(ProviderType providerType, String providerId); + + void disconnect(ProviderType providerType, String providerId); +} diff --git a/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginServiceFactory.java b/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginServiceFactory.java new file mode 100644 index 00000000..2952f076 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/oauth2/service/SocialLoginServiceFactory.java @@ -0,0 +1,19 @@ +package org.chzz.market.domain.oauth2.service; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.user.entity.User.ProviderType; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SocialLoginServiceFactory { + private final KakaoSocialLoginService kakaoSocialLoginService; + private final NaverSocialLoginService naverSocialLoginService; + + public SocialLoginService getService(ProviderType providerType) { + return switch (providerType) { + case KAKAO -> kakaoSocialLoginService; + case NAVER -> naverSocialLoginService; + }; + } +} diff --git a/src/main/java/org/chzz/market/domain/token/service/TokenService.java b/src/main/java/org/chzz/market/domain/token/service/TokenService.java index 52675bc9..34c95b43 100644 --- a/src/main/java/org/chzz/market/domain/token/service/TokenService.java +++ b/src/main/java/org/chzz/market/domain/token/service/TokenService.java @@ -50,9 +50,10 @@ public Map reissue(String refreshToken) { public void logout(String refreshToken) { jwtUtil.validateToken(refreshToken, TokenType.REFRESH); - Long userId = refreshTokenRepository.findByToken(refreshToken) - .orElseThrow(() -> new TokenException(TokenErrorCode.REFRESH_TOKEN_NOT_FOUND)).userId(); - refreshTokenRepository.deleteByToken(refreshToken); - log.info("사용자 ID {}: 로그아웃이 완료되었습니다.", userId); + + refreshTokenRepository.findByToken(refreshToken).ifPresent(tokenData -> { + refreshTokenRepository.deleteByToken(refreshToken); + log.info("사용자 ID {}: 로그아웃이 완료되었습니다.", tokenData.userId()); + }); } } diff --git a/src/main/java/org/chzz/market/domain/user/controller/UserApi.java b/src/main/java/org/chzz/market/domain/user/controller/UserApi.java index dc7ccc2e..95038e23 100644 --- a/src/main/java/org/chzz/market/domain/user/controller/UserApi.java +++ b/src/main/java/org/chzz/market/domain/user/controller/UserApi.java @@ -1,15 +1,22 @@ package org.chzz.market.domain.user.controller; +import static org.chzz.market.domain.user.error.UserErrorCode.Const.CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS; +import static org.chzz.market.domain.user.error.UserErrorCode.Const.CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.util.Map; +import org.chzz.market.common.config.LoginUser; +import org.chzz.market.common.springdoc.ApiExceptionExplanation; +import org.chzz.market.common.springdoc.ApiResponseExplanations; import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest; import org.chzz.market.domain.user.dto.request.UserCreateRequest; import org.chzz.market.domain.user.dto.response.NicknameAvailabilityResponse; import org.chzz.market.domain.user.dto.response.UserProfileResponse; +import org.chzz.market.domain.user.error.UserErrorCode; import org.hibernate.validator.constraints.Length; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; @@ -38,4 +45,14 @@ ResponseEntity completeRegistration(Long userId, @Valid UserCreateRequest @Operation(summary = "로그아웃") ResponseEntity logout(HttpServletRequest request, HttpServletResponse response); + + @Operation(summary = "회원탈퇴", description = "회원 탈퇴 시 진행 중인 경매나 입찰이 있을 경우 탈퇴가 불가능합니다.") + @ApiResponseExplanations( + errors = { + @ApiExceptionExplanation(value = UserErrorCode.class, constant = CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS, name = "진행 중인 경매가 있어 회원 탈퇴가 불가능한 경우"), + @ApiExceptionExplanation(value = UserErrorCode.class, constant = CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS, name = "진행 중인 입찰이 있어 회원 탈퇴가 불가능한 경우"), + } + ) + ResponseEntity deleteUser(@LoginUser Long userId, HttpServletRequest request, HttpServletResponse response); + } diff --git a/src/main/java/org/chzz/market/domain/user/controller/UserController.java b/src/main/java/org/chzz/market/domain/user/controller/UserController.java index f275bac5..e7a9be9c 100644 --- a/src/main/java/org/chzz/market/domain/user/controller/UserController.java +++ b/src/main/java/org/chzz/market/domain/user/controller/UserController.java @@ -18,10 +18,12 @@ import org.chzz.market.domain.user.dto.response.NicknameAvailabilityResponse; import org.chzz.market.domain.user.dto.response.UserProfileResponse; import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.service.UserDeleteService; import org.chzz.market.domain.user.service.UserService; import org.hibernate.validator.constraints.Length; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -37,6 +39,7 @@ @Slf4j public class UserController implements UserApi { private final UserService userService; + private final UserDeleteService userDeleteService; private final TokenService tokenService; /** @@ -126,4 +129,15 @@ public ResponseEntity logout(HttpServletRequest request, HttpServletRespon CookieUtil.expireCookie(response, TokenType.REFRESH.name()); return ResponseEntity.ok().build(); } + + @Override + @DeleteMapping + public ResponseEntity deleteUser(@LoginUser Long userId, HttpServletRequest request, + HttpServletResponse response) { + userDeleteService.delete(userId); + String refreshToken = CookieUtil.getCookieByNameOrThrow(request, TokenType.REFRESH.name()); + tokenService.logout(refreshToken); + CookieUtil.expireCookie(response, TokenType.REFRESH.name()); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/org/chzz/market/domain/user/dto/CustomUserDetails.java b/src/main/java/org/chzz/market/domain/user/dto/CustomUserDetails.java index 6b006e5d..066e3e34 100644 --- a/src/main/java/org/chzz/market/domain/user/dto/CustomUserDetails.java +++ b/src/main/java/org/chzz/market/domain/user/dto/CustomUserDetails.java @@ -35,6 +35,10 @@ public Collection getAuthorities() { @Override public String getName() { - return user.getEmail(); + return String.valueOf(user.getId()); + } + + public String getProviderId() { + return user.getProviderId(); } } diff --git a/src/main/java/org/chzz/market/domain/user/dto/UserDeletedEvent.java b/src/main/java/org/chzz/market/domain/user/dto/UserDeletedEvent.java new file mode 100644 index 00000000..d2f49ebf --- /dev/null +++ b/src/main/java/org/chzz/market/domain/user/dto/UserDeletedEvent.java @@ -0,0 +1,6 @@ +package org.chzz.market.domain.user.dto; + +import org.chzz.market.domain.user.entity.User.ProviderType; + +public record UserDeletedEvent(ProviderType type, String providerId) { +} diff --git a/src/main/java/org/chzz/market/domain/user/entity/User.java b/src/main/java/org/chzz/market/domain/user/entity/User.java index da22cff5..f97ec31a 100644 --- a/src/main/java/org/chzz/market/domain/user/entity/User.java +++ b/src/main/java/org/chzz/market/domain/user/entity/User.java @@ -53,7 +53,6 @@ public class User extends BaseTimeEntity { private String profileImageUrl; - // 구현 방식에 따라 권한 설정이 달라질 수 있어 임의로 열거체 선언 하였습니다 @Column(columnDefinition = "varchar(20)") @Enumerated(EnumType.STRING) private UserRole userRole; @@ -62,7 +61,7 @@ public class User extends BaseTimeEntity { @Enumerated(EnumType.STRING) private ProviderType providerType; - @Column(columnDefinition = "binary(16)", unique = true, nullable = false) + @Column(columnDefinition = "binary(16)", unique = true) private UUID customerKey; @PrePersist @@ -93,12 +92,24 @@ public void updateProfile(UpdateUserProfileRequest request, String profileImageU this.profileImageUrl = profileImageUrl; } + public void anonymize() { + this.userRole = UserRole.DELETED_USER; + this.email = null; + this.nickname = "탈퇴한 사용자"; + this.bio = null; + this.profileImageUrl = null; + this.providerId = null; + this.providerType = null; + this.customerKey = null; + } + @Getter @AllArgsConstructor public enum UserRole { TEMP_USER("ROLE_TEMP_USER"), USER("ROLE_USER"), - ADMIN("ROLE_ADMIN"); + ADMIN("ROLE_ADMIN"), + DELETED_USER("ROLE_DELETED_USER"); private final String value; } diff --git a/src/main/java/org/chzz/market/domain/user/error/UserErrorCode.java b/src/main/java/org/chzz/market/domain/user/error/UserErrorCode.java index 5e607438..4bdd5c18 100644 --- a/src/main/java/org/chzz/market/domain/user/error/UserErrorCode.java +++ b/src/main/java/org/chzz/market/domain/user/error/UserErrorCode.java @@ -1,5 +1,9 @@ package org.chzz.market.domain.user.error; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + import lombok.AllArgsConstructor; import lombok.Getter; import org.chzz.market.common.error.ErrorCode; @@ -8,20 +12,28 @@ @Getter @AllArgsConstructor public enum UserErrorCode implements ErrorCode { - NICKNAME_DUPLICATION(HttpStatus.BAD_REQUEST, "닉네임이 중복되었습니다."), - USER_NOT_MATCHED(HttpStatus.BAD_REQUEST, "사용자 정보가 일치하지 않습니다."), - USER_ALREADY_REGISTERED(HttpStatus.BAD_REQUEST, "이미 가입된 사용자입니다."), - UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "권한이 없는 사용자입니다."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."); + CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS(BAD_REQUEST, "등록한 진행 중인 경매가 있어 회원 탈퇴가 불가능합니다."), + CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS(BAD_REQUEST, "참여 중인 입찰이 있어 회원 탈퇴가 불가능합니다."), + NICKNAME_DUPLICATION(BAD_REQUEST, "닉네임이 중복되었습니다."), + USER_NOT_MATCHED(BAD_REQUEST, "사용자 정보가 일치하지 않습니다."), + USER_ALREADY_REGISTERED(BAD_REQUEST, "이미 가입된 사용자입니다."), + UNAUTHORIZED_USER(UNAUTHORIZED, "권한이 없는 사용자입니다."), + USER_NOT_FOUND(NOT_FOUND, "사용자를 찾을 수 없습니다."), + KAKAO_UNLINK_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 연결 끊기에 실패했습니다."), + NAVER_UNLINK_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "네이버 연결 끊기에 실패했습니다."); private final HttpStatus httpStatus; private final String message; public static class Const { + public static final String CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS = "CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS"; + public static final String CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS = "CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS"; public static final String NICKNAME_DUPLICATION = "NICKNAME_DUPLICATION"; public static final String USER_NOT_MATCHED = "USER_NOT_MATCHED"; public static final String USER_ALREADY_REGISTERED = "USER_ALREADY_REGISTERED"; public static final String UNAUTHORIZED_USER = "UNAUTHORIZED_USER"; public static final String USER_NOT_FOUND = "USER_NOT_FOUND"; + public static final String KAKAO_UNLINK_FAILED = "KAKAO_UNLINK_FAILED"; + public static final String NAVER_UNLINK_FAILED = "NAVER_UNLINK_FAILED"; } } diff --git a/src/main/java/org/chzz/market/domain/user/service/UserDeleteService.java b/src/main/java/org/chzz/market/domain/user/service/UserDeleteService.java new file mode 100644 index 00000000..7a43a0f1 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/user/service/UserDeleteService.java @@ -0,0 +1,58 @@ +package org.chzz.market.domain.user.service; + +import static org.chzz.market.domain.user.error.UserErrorCode.CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS; +import static org.chzz.market.domain.user.error.UserErrorCode.CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS; +import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; + +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.domain.auction.repository.AuctionQueryRepository; +import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.user.dto.UserDeletedEvent; +import org.chzz.market.domain.user.entity.User; +import org.chzz.market.domain.user.entity.User.ProviderType; +import org.chzz.market.domain.user.error.exception.UserException; +import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserDeleteService { + private final UserRepository userRepository; + private final AuctionRepository auctionRepository; + private final AuctionQueryRepository auctionQueryRepository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public void delete(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + // 1. 탈퇴 가능 여부 체크 + checkDeletable(userId); + + // 2. 정보 삭제 전 소셜 로그인 연결 정보 저장 + ProviderType type = user.getProviderType(); + String providerId = user.getProviderId(); + + // 3. 정보 삭제 + user.anonymize(); + + // 4. 소셜 로그인 연결 끊기 + eventPublisher.publishEvent(new UserDeletedEvent(type, providerId)); + } + + private void checkDeletable(Long userId) { + //1. 현재 등록한 경매 중 진행중이 있는지 + long proceedingAuctionCount = auctionRepository.countBySellerIdAndStatusIn(userId, AuctionStatus.PROCEEDING); + if (proceedingAuctionCount > 0) { + throw new UserException(CANNOT_DELETE_USER_DUE_TO_ONGOING_AUCTIONS); + } + //2. 현재 입찰 진행 중인 경매 가 있는지 + long proceedingBidCount = auctionQueryRepository.countProceedingAuctionsByUserId(userId); + if (proceedingBidCount > 0) { + throw new UserException(CANNOT_DELETE_USER_DUE_TO_ONGOING_BIDS); + } + } +} diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 4a1d5cb9..cabb0fe8 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -95,3 +95,7 @@ logging: client: url: "http://test" + +oauth2: + kakao: + rest-api-key: testapikey diff --git a/src/main/resources/db/migration/V2__modify_user_field.sql b/src/main/resources/db/migration/V2__modify_user_field.sql new file mode 100644 index 00000000..98b81a0d --- /dev/null +++ b/src/main/resources/db/migration/V2__modify_user_field.sql @@ -0,0 +1,4 @@ +ALTER TABLE users + MODIFY provider_id VARCHAR(255) NULL, + MODIFY email VARCHAR(255) NULL, + MODIFY customer_key BINARY(16) NULL; diff --git a/src/test/java/org/chzz/market/domain/token/service/TokenServiceTest.java b/src/test/java/org/chzz/market/domain/token/service/TokenServiceTest.java index b23e474c..51f0a2ef 100644 --- a/src/test/java/org/chzz/market/domain/token/service/TokenServiceTest.java +++ b/src/test/java/org/chzz/market/domain/token/service/TokenServiceTest.java @@ -110,19 +110,8 @@ void reissue_ThrowsException_WhenTokenNotFound() { when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.empty()); // when & then - TokenException exception = assertThrows(TokenException.class, () -> tokenService.reissue(refreshCookie.getValue())); - assertThat(exception.getErrorCode()).isEqualTo(TokenErrorCode.REFRESH_TOKEN_NOT_FOUND); - } - - @Test - @DisplayName("로그아웃 시 Refresh Token이 존재하지 않을 때 예외 발생 테스트") - void logout_ThrowsException_WhenTokenNotFound() { - // given - Cookie refreshCookie = new Cookie("refresh-token", "refresh-token"); - when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.empty()); - - // when & then - TokenException exception = assertThrows(TokenException.class, () -> tokenService.logout(refreshCookie.getValue())); + TokenException exception = assertThrows(TokenException.class, + () -> tokenService.reissue(refreshCookie.getValue())); assertThat(exception.getErrorCode()).isEqualTo(TokenErrorCode.REFRESH_TOKEN_NOT_FOUND); } } diff --git a/src/test/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandlerTest.java b/src/test/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandlerTest.java index 2d4c9214..f0fa93de 100644 --- a/src/test/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandlerTest.java +++ b/src/test/java/org/chzz/market/domain/user/oauth2/CustomSuccessHandlerTest.java @@ -1,7 +1,6 @@ package org.chzz.market.domain.user.oauth2; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.given; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; @@ -12,6 +11,7 @@ import java.io.IOException; import java.lang.reflect.Field; import org.chzz.market.common.filter.HttpCookieOAuth2AuthorizationRequestRepository; +import org.chzz.market.domain.oauth2.service.CustomSuccessHandler; import org.chzz.market.domain.token.service.TokenService; import org.chzz.market.domain.user.dto.CustomUserDetails; import org.chzz.market.domain.user.entity.User; @@ -21,7 +21,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.core.Authentication; From 74939d617aa6d579f88e5b02f2087dbeef418cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=98=88=EC=B0=AC?= <88381563+YeaChan05@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:10:40 +0900 Subject: [PATCH 15/16] =?UTF-8?q?feat:=20Presigned=20URL=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=A0=81=EC=9A=A9=20(#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 버킷 이름 등록 경매 이미지와 프로필 이미지 s3 bucket prefix를 enum으로 등록 * feat: 버킷 이름 검증기 구현 애플리케이션 로드 시점에 bucket의 prefix를 검증하는 검증기 구현 * move: 패키지 이동 config -> config/aws * test: 테스트 방식 수정 테스트에 사용하던 `AmazonS3` 빈의 mocking을 통해 새로운 설정값에 대한 Context 세팅 수정 * feat: 실제 경로 생성기 구현 실제 prefix에 업로드되는 경로 생성 * feat: 로컬 이미지 수정 로컬에서 동작할 화면단 이미지를 실제 애플리케이션 이미지로 수정 * fix: 이미지 prefix 수정 이미지 fileId를 추가 prefix로 지정 * fix: 경매 등록 요청 객체 수정 `MultipartFIle`을 업로드한 이미지 object key로 변경 * fix: 경매 엔드포인트에서 `MultipartFIle` 제거 `MultipartFIle`의 필요성이 없어짐에 따른 엔트포인트 전달 매개변수 수정 * remove: `MultipartFIle`검증 어노테이션 제거 `MultipartFIle` 제거에 따른 검증 어노테이션 제거 * refacfor: 필드 추가 및 필드명 변경 1. 필드명 변경 `productName`=>`auctionName` 2.object key를 매핑할 Map 추가 * fix: `MultipartFIle` 제거에 따른 서비스 변경 요청 방식이 s3 object key 업로드 방식으로 바뀜에 따른 서비스 변경 * feat: 이미지 업로드 인증 서비스 구현 이미지를 직접 클라이언트가 업로드 하기 위해 필요한 url을 제공해주는 서비스 구현 * feat: presigned url 발급 엔드포인트 구현 * fix: `MultipartFile` 제거에 따른 이벤트 객체 수정 * fix: `MultipartFile` 제거에 따른 요청 객체 수정 * fix: 회원 프로필 수정 엔드포인트 수정 `MultipartFile` 제거에 따른 회원 프로필 수정시 이미지 변경 방식 수정 * feat: presigned 응답 객체 presigned url 응답 객체 구현 * fix: 요청 검증 수정 이미지가 없는 경우를 검증하는 어노테이션의 메세지 수정 * fix: 기본 이미지 적용 로직 추가 요청값에 따라 기본 이미지를 적용하는 로직 추가 * test: API 변경에 따른 테스트 수정 API 변경에 따른 테스트 수정ˆ * fix: 경매 이미지 업로드 방식 수정 경매 이미지 업로드시 하나의 경매에 대해 동일한 fileId를 제공하고 각 파일명을 해시값으로 구분 * fix: 요청 본문 검증 요청 본문 검증 어노테이션 적용 * refactor: 매핑 정보 명시 매핑 정보를 구현체에 명시 * refactor: 공백 추가 글 사이 공백 추가 * fix: 불필요한 어노테이션 제거 `@RequestPart` 제거 * fix: 이미지 path 수정 `s3BucketName` -> `cloudfrontDomain` * refactor: 코드 공백 추가 코드 공백 추가 * refactor: 필드명 변경 경매 등록시 경매 이름을 `auctionName`로 변경 * refactor: 필드명 변경 경매 등록시 경매 이름을 `auctionName`로 변경 * test: 설정값 수정 설정값 주입 `s3BucketName`->`cloudFrontDomain` * feat: object key 검증기 추가 미확인된 object key의 유효성 검증 * fix: object key 검증 로직 추가 * remove: 불필요한 서비스 제거 * test: 검증 mocking object key 검증을 mocking * refactor: 변수명 변경 모든 `productName` -> `auctionName` * remove: 테스트 데이터파일 제거 불필요한 테스트 데이터파일 제거 * test: 필드 수정 `getProductName` -> `getAuctionName` --- compose-local.yaml | 4 +- .../common/config/{ => aws}/AWSConfig.java | 2 +- .../common/config/aws/BucketPrefix.java | 24 ++ .../common/config/aws/S3PrefixVerifier.java | 35 ++ .../market/common/error/GlobalErrorCode.java | 2 +- .../annotation/NotEmptyMultipartList.java | 34 -- .../NotEmptyMultipartListValidator.java | 22 -- .../domain/auction/controller/AuctionApi.java | 9 +- .../auction/controller/AuctionController.java | 14 +- .../auction/controller/AuctionDetailApi.java | 8 +- .../controller/AuctionDetailController.java | 14 +- .../auction/dto/AuctionImageUpdateEvent.java | 6 +- .../domain/auction/dto/ImageUploadEvent.java | 3 +- .../auction/dto/request/RegisterRequest.java | 10 +- .../dto/request/UpdateAuctionRequest.java | 5 +- .../response/BaseAuctionDetailResponse.java | 6 +- .../dto/response/BaseAuctionResponse.java | 6 +- .../dto/response/EndedAuctionResponse.java | 4 +- .../dto/response/LostAuctionResponse.java | 4 +- .../OfficialAuctionDetailResponse.java | 4 +- .../dto/response/OfficialAuctionResponse.java | 4 +- .../response/PreAuctionDetailResponse.java | 4 +- .../dto/response/PreAuctionResponse.java | 4 +- .../response/ProceedingAuctionResponse.java | 4 +- .../response/WonAuctionDetailsResponse.java | 2 +- .../dto/response/WonAuctionResponse.java | 4 +- .../market/domain/auction/entity/Auction.java | 2 +- .../auction/service/AuctionEndService.java | 18 +- .../auction/service/AuctionModifyService.java | 20 +- .../service/AuctionRegistrationService.java | 13 +- .../PreAuctionRegistrationService.java | 13 +- .../auction/service/RegistrationService.java | 4 +- .../bid/dto/response/BiddingRecord.java | 4 +- .../domain/image/controller/ImageApi.java | 41 ++ .../image/controller/ImageController.java | 33 ++ .../response/CreatePresignedUrlResponse.java | 11 + .../domain/image/error/ImageErrorCode.java | 4 +- .../domain/image/service/ImageService.java | 115 ++---- .../image/service/ImageUploadService.java | 55 +++ .../image/service/ObjectKeyValidator.java | 20 + .../domain/image/service/S3ImageUploader.java | 44 --- .../notification/entity/NotificationType.java | 5 +- .../domain/user/controller/UserApi.java | 2 +- .../user/controller/UserController.java | 3 +- .../dto/request/UpdateUserProfileRequest.java | 2 + .../domain/user/service/UserService.java | 18 +- src/main/resources/db/data.sql | 357 ------------------ .../chzz/market/MarketApplicationTests.java | 3 + .../org/chzz/market/common/AWSConfig.java | 21 +- .../controller/AuctionControllerTest.java | 106 +++--- .../AuctionQueryRepositoryTest.java | 51 +-- .../service/AuctionLookupServiceTest.java | 3 + .../repository/BidQueryRepositoryTest.java | 5 +- .../bid/service/BidCancelLockServiceTest.java | 3 + .../BidCreateServiceConcurrencyTest.java | 3 + .../LikeUpdateServiceConcurrencyTest.java | 3 + .../domain/user/service/UserServiceTest.java | 45 +-- .../market/util/AuthenticatedRequestTest.java | 3 + 58 files changed, 477 insertions(+), 791 deletions(-) rename src/main/java/org/chzz/market/common/config/{ => aws}/AWSConfig.java (96%) create mode 100644 src/main/java/org/chzz/market/common/config/aws/BucketPrefix.java create mode 100644 src/main/java/org/chzz/market/common/config/aws/S3PrefixVerifier.java delete mode 100644 src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java delete mode 100644 src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java create mode 100644 src/main/java/org/chzz/market/domain/image/controller/ImageApi.java create mode 100644 src/main/java/org/chzz/market/domain/image/controller/ImageController.java create mode 100644 src/main/java/org/chzz/market/domain/image/dto/response/CreatePresignedUrlResponse.java create mode 100644 src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java create mode 100644 src/main/java/org/chzz/market/domain/image/service/ObjectKeyValidator.java delete mode 100644 src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java delete mode 100644 src/main/resources/db/data.sql diff --git a/compose-local.yaml b/compose-local.yaml index b896106b..5b3773ae 100644 --- a/compose-local.yaml +++ b/compose-local.yaml @@ -28,10 +28,10 @@ services: - "6379:6379" chzz-frontend: - image: junest1010/test-app:latest + image: cloudoort/chzzmarket-frontend:1.0 container_name: react-app ports: - - "3000:3000" + - "5173:5173" # node-exporter: # image: prom/node-exporter:latest diff --git a/src/main/java/org/chzz/market/common/config/AWSConfig.java b/src/main/java/org/chzz/market/common/config/aws/AWSConfig.java similarity index 96% rename from src/main/java/org/chzz/market/common/config/AWSConfig.java rename to src/main/java/org/chzz/market/common/config/aws/AWSConfig.java index 43c1183a..f9252297 100644 --- a/src/main/java/org/chzz/market/common/config/AWSConfig.java +++ b/src/main/java/org/chzz/market/common/config/aws/AWSConfig.java @@ -1,4 +1,4 @@ -package org.chzz.market.common.config; +package org.chzz.market.common.config.aws; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; diff --git a/src/main/java/org/chzz/market/common/config/aws/BucketPrefix.java b/src/main/java/org/chzz/market/common/config/aws/BucketPrefix.java new file mode 100644 index 00000000..b3e0defc --- /dev/null +++ b/src/main/java/org/chzz/market/common/config/aws/BucketPrefix.java @@ -0,0 +1,24 @@ +package org.chzz.market.common.config.aws; + +import java.util.Arrays; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BucketPrefix { + AUCTION("auction"), + PROFILE("profile"); + private final String name; + + public static boolean hasNameOf(String name) { + return Arrays.stream(values()) + .anyMatch(bucketFolderName -> bucketFolderName.name.equals(name)); + } + + public String createPath(final String fileName) { + String fileId = UUID.randomUUID().toString(); + return String.format("%s/%s/%s", this.name, fileId, fileName); + } +} diff --git a/src/main/java/org/chzz/market/common/config/aws/S3PrefixVerifier.java b/src/main/java/org/chzz/market/common/config/aws/S3PrefixVerifier.java new file mode 100644 index 00000000..201649a4 --- /dev/null +++ b/src/main/java/org/chzz/market/common/config/aws/S3PrefixVerifier.java @@ -0,0 +1,35 @@ +package org.chzz.market.common.config.aws; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ListObjectsV2Request; +import com.amazonaws.services.s3.model.ListObjectsV2Result; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 이미지가 업로드 가능한 파일 목록 세팅 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class S3PrefixVerifier { + private static final String DELIMITER = "/"; + + private final AmazonS3 s3; + private final String bucket; + + @EventListener(ApplicationReadyEvent.class) + private boolean verifyPrefix() { + ListObjectsV2Request req = new ListObjectsV2Request() + .withBucketName(bucket) + .withDelimiter(DELIMITER); + ListObjectsV2Result result = s3.listObjectsV2(req); + return result.getCommonPrefixes().stream() + .map(prefix -> prefix.split(DELIMITER)[0]) + .peek(prefix -> log.info("bucket prefix: {}", prefix)) + .allMatch(BucketPrefix::hasNameOf); + } +} diff --git a/src/main/java/org/chzz/market/common/error/GlobalErrorCode.java b/src/main/java/org/chzz/market/common/error/GlobalErrorCode.java index 0a8f1df8..f17c80de 100644 --- a/src/main/java/org/chzz/market/common/error/GlobalErrorCode.java +++ b/src/main/java/org/chzz/market/common/error/GlobalErrorCode.java @@ -9,7 +9,7 @@ public enum GlobalErrorCode implements ErrorCode { INVALID_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid request parameter"), UNSUPPORTED_PARAMETER_TYPE(HttpStatus.BAD_REQUEST, "Unsupported type of parameter included"), - UNSUPPORTED_PARAMETER_NAME(HttpStatus.BAD_REQUEST, "Unsupported productName of parameter included"), + UNSUPPORTED_PARAMETER_NAME(HttpStatus.BAD_REQUEST, "Unsupported auctionName of parameter included"), VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "Validation failed"), COOKIE_NOT_FOUND(HttpStatus.BAD_REQUEST, "Required cookie is not found"), AUTHENTICATION_REQUIRED(HttpStatus.UNAUTHORIZED, "Authentication is required"), diff --git a/src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java b/src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java deleted file mode 100644 index 588575de..00000000 --- a/src/main/java/org/chzz/market/common/validation/annotation/NotEmptyMultipartList.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.chzz.market.common.validation.annotation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.chzz.market.common.validation.annotation.NotEmptyMultipartList.List; -import org.chzz.market.common.validation.validator.NotEmptyMultipartListValidator; - -@Documented -@Constraint( - validatedBy = {NotEmptyMultipartListValidator.class} -) -@Target(ElementType.PARAMETER) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(List.class) -public @interface NotEmptyMultipartList { - String message() default "파일은 최소 하나 이상 필요합니다."; - - Class[] groups() default {}; - - Class[] payload() default {}; - - @Target(ElementType.PARAMETER) - @Retention(RetentionPolicy.RUNTIME) - @Documented - public @interface List { - NotEmptyMultipartList[] value(); - } -} diff --git a/src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java b/src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java deleted file mode 100644 index 50d3068e..00000000 --- a/src/main/java/org/chzz/market/common/validation/validator/NotEmptyMultipartListValidator.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.chzz.market.common.validation.validator; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import java.util.Collection; -import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; -import org.springframework.web.multipart.MultipartFile; - -public class NotEmptyMultipartListValidator implements - ConstraintValidator> { - - @Override - public boolean isValid(final Collection multipartFiles, - final ConstraintValidatorContext context) { - for (MultipartFile file : multipartFiles) { - if (file.isEmpty()) { - return false; - } - } - return true; - } -} diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java index 1505bb26..c5552023 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionApi.java @@ -12,12 +12,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Size; import java.util.List; import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; -import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; import org.chzz.market.domain.auction.dto.request.RegisterRequest; import org.chzz.market.domain.auction.dto.response.CategoryResponse; import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; @@ -37,10 +35,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.multipart.MultipartFile; @Tag(name = "auctions", description = "경매 API") @RequestMapping("/v1/auctions") @@ -118,9 +115,7 @@ ResponseEntity> getLikedAuctionList(@LoginUser Long use ) @PostMapping ResponseEntity registerAuction(@LoginUser Long userId, - @RequestPart("request") @Valid RegisterRequest request, - @RequestPart(value = "images") @Valid - @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images); + @RequestBody @Valid RegisterRequest request); @Operation(summary = "경매 테스트 등록", description = "테스트 등록합니다.") @PostMapping("/test") diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java index ea2eb926..07e94c45 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionController.java @@ -3,11 +3,9 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Size; import java.util.List; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; -import org.chzz.market.common.validation.annotation.NotEmptyMultipartList; import org.chzz.market.domain.auction.dto.AuctionRegisterType; import org.chzz.market.domain.auction.dto.request.RegisterRequest; import org.chzz.market.domain.auction.dto.response.CategoryResponse; @@ -26,15 +24,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -130,13 +126,11 @@ public ResponseEntity> getLikedAuctionList(@LoginUser L * 경매 등록 */ @Override - @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @PostMapping public ResponseEntity registerAuction(@LoginUser Long userId, - @RequestPart("request") @Valid RegisterRequest request, - @RequestPart(value = "images") @Valid - @NotEmptyMultipartList @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") List images) { + @RequestBody @Valid RegisterRequest request) { AuctionRegisterType type = request.auctionRegisterType(); - type.getService().register(userId, request, images);//요청 타입에 따라 다른 서비스 호출 + type.getService().register(userId, request);//요청 타입에 따라 다른 서비스 호출 return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java index f4d45443..f00e56dc 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailApi.java @@ -20,7 +20,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.Map; import org.chzz.market.common.config.LoginUser; import org.chzz.market.common.springdoc.ApiExceptionExplanation; import org.chzz.market.common.springdoc.ApiResponseExplanations; @@ -40,9 +39,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.RequestBody; @Tag(name = "auctions", description = "경매 API") public interface AuctionDetailApi { @@ -114,8 +111,7 @@ ResponseEntity likeAuction(@LoginUser Long userId, ) ResponseEntity updateAuction(@LoginUser Long userId, @PathVariable Long auctionId, - @RequestPart @Valid UpdateAuctionRequest request, - @RequestParam(required = false) Map images); + @RequestBody @Valid UpdateAuctionRequest request); @Operation(summary = "특정 경매 삭제", description = "특정 경매를 삭제합니다. 삭제는 사전경매만 가능합니다.") @ApiResponseExplanations( diff --git a/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java index 3818066c..048e1100 100644 --- a/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java +++ b/src/main/java/org/chzz/market/domain/auction/controller/AuctionDetailController.java @@ -2,7 +2,7 @@ import static org.springframework.data.domain.Sort.Direction.DESC; -import java.util.Map; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.chzz.market.common.config.LoginUser; import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; @@ -19,16 +19,15 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -80,12 +79,11 @@ public ResponseEntity likeAuction(@LoginUser Long userId, @PathVariable Lo } @Override - @PatchMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity updateAuction(Long userId, @PathVariable Long auctionId, - UpdateAuctionRequest request, - Map images) { + @PatchMapping + public ResponseEntity updateAuction(@LoginUser Long userId, @PathVariable Long auctionId, + @RequestBody @Valid UpdateAuctionRequest request) { UpdateAuctionResponse response = - auctionModifyService.updateAuction(userId, auctionId, request, images); + auctionModifyService.updateAuction(userId, auctionId, request); return ResponseEntity.ok(response); } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java b/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java index ccce5396..92604c63 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/AuctionImageUpdateEvent.java @@ -1,11 +1,9 @@ package org.chzz.market.domain.auction.dto; import java.util.Map; -import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; import org.chzz.market.domain.auction.entity.Auction; -import org.springframework.web.multipart.MultipartFile; public record AuctionImageUpdateEvent(Auction auction, - UpdateAuctionRequest request, - Map imageBuffer) { + Map imageSequence, + Map objectKeyBuffer) { } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java b/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java index ed237add..c36bd9db 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/ImageUploadEvent.java @@ -2,7 +2,6 @@ import java.util.List; import org.chzz.market.domain.auction.entity.Auction; -import org.springframework.web.multipart.MultipartFile; -public record ImageUploadEvent(Auction auction, List images) { +public record ImageUploadEvent(Auction auction, List objectKeys) { } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java index ff02ac35..561e677e 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/request/RegisterRequest.java @@ -2,15 +2,17 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import java.util.List; import org.chzz.market.common.validation.annotation.ThousandMultiple; import org.chzz.market.domain.auction.dto.AuctionRegisterType; import org.chzz.market.domain.auction.entity.Category; public record RegisterRequest( - String productName, + String auctionName, @Schema(description = "개행문자 포함 최대 1000자, 개행문자 최대 10개") @Size(max = 1000, message = "상품설명은 1000자 이내여야 합니다.") @@ -26,7 +28,11 @@ public record RegisterRequest( Integer minPrice, @NotNull(message = "경매 타입을 선택해주세요") - AuctionRegisterType auctionRegisterType + AuctionRegisterType auctionRegisterType, + + @NotEmpty(message = "파일은 최소 하나 이상 필요합니다.") + @Size(max = 5, message = "이미지는 5장 이내로만 업로드 가능합니다.") + List objectKeys ) { private static final String DESCRIPTION_REGEX = "^(?:(?:[^\\n]*\\n){0,10}[^\\n]*$)"; // 개행문자 10개를 제한 } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java b/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java index 8802c186..0410eeec 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/request/UpdateAuctionRequest.java @@ -21,7 +21,7 @@ public class UpdateAuctionRequest { public static final String DESCRIPTION_REGEX = "^(?:(?:[^\\n]*\\n){0,10}[^\\n]*$)"; // 개행문자 10개를 제한 @Size(min = 2, max = 30, message = "제목은 최소 2글자 이상 30자 이하여야 합니다") - private String productName; + private String auctionName; @Schema(description = "개행문자 포함 최대 1000자, 개행문자 최대 10개") @Size(max = 1000, message = "상품설명은 1000자 이내여야 합니다.") @@ -36,5 +36,8 @@ public class UpdateAuctionRequest { @Builder.Default private Map imageSequence = new HashMap<>(); + + @Builder.Default + private final Map objectKeyBuffer = new HashMap<>(); } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java index 6fe1f9ba..02f997aa 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionDetailResponse.java @@ -13,7 +13,7 @@ public abstract class BaseAuctionDetailResponse { private Long auctionId; private String sellerNickname; private String sellerProfileImageUrl; - private String productName; + private String auctionName; private String description; private Integer minPrice; protected Boolean isSeller; @@ -22,12 +22,12 @@ public abstract class BaseAuctionDetailResponse { private List images; public BaseAuctionDetailResponse(Long auctionId, String sellerNickname, String sellerProfileImageUrl, - String productName, String description, Integer minPrice, Boolean isSeller, + String auctionName, String description, Integer minPrice, Boolean isSeller, AuctionStatus status, Category category) { this.auctionId = auctionId; this.sellerNickname = sellerNickname; this.sellerProfileImageUrl = sellerProfileImageUrl; - this.productName = productName; + this.auctionName = auctionName; this.description = description; this.minPrice = minPrice; this.isSeller = isSeller; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java index 5619a4c3..17dca7ce 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/BaseAuctionResponse.java @@ -7,14 +7,14 @@ @NoArgsConstructor public abstract class BaseAuctionResponse { private Long auctionId; - private String productName; + private String auctionName; private String imageUrl; private Long minPrice; private Boolean isSeller; - public BaseAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller) { + public BaseAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller) { this.auctionId = auctionId; - this.productName = productName; + this.auctionName = auctionName; this.imageUrl = imageUrl; this.minPrice = minPrice; this.isSeller = isSeller; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java index cf667a2a..063b1da7 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/EndedAuctionResponse.java @@ -13,10 +13,10 @@ public class EndedAuctionResponse extends BaseAuctionResponse { private Boolean isOrdered; private LocalDateTime createAt; - public EndedAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + public EndedAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long participantCount, Long winningBidAmount, Boolean isWon, Boolean isOrdered, LocalDateTime createAt) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.participantCount = participantCount; this.winningBidAmount = winningBidAmount; this.isWon = isWon; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java index abca1eaa..e8ff8d53 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/LostAuctionResponse.java @@ -11,9 +11,9 @@ public class LostAuctionResponse extends BaseAuctionResponse { private LocalDateTime endDateTime; private Long bidAmount; - public LostAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + public LostAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long participantCount, LocalDateTime endDateTime, Long bidAmount) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.participantCount = participantCount; this.endDateTime = endDateTime; this.bidAmount = bidAmount; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java index fc4b7aac..0be714d7 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionDetailResponse.java @@ -26,12 +26,12 @@ public class OfficialAuctionDetailResponse extends BaseAuctionDetailResponse { private Boolean isOrdered; public OfficialAuctionDetailResponse(Long auctionId, String sellerNickname, String sellerProfileImageUrl, - String productName, String description, Integer minPrice, Boolean isSeller, + String auctionName, String description, Integer minPrice, Boolean isSeller, AuctionStatus status, Category category, Long timeRemaining, Long participantCount, Boolean isParticipated, Long bidId, Long bidAmount, int remainingBidCount, Boolean isCancelled, Boolean isWinner, Boolean isWon, Boolean isOrdered) { - super(auctionId, sellerNickname, sellerProfileImageUrl, productName, description, minPrice, isSeller, status, + super(auctionId, sellerNickname, sellerProfileImageUrl, auctionName, description, minPrice, isSeller, status, category); this.timeRemaining = timeRemaining; this.participantCount = participantCount; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java index 8c515aa6..f2c48f28 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/OfficialAuctionResponse.java @@ -10,9 +10,9 @@ public class OfficialAuctionResponse extends BaseAuctionResponse { private Long participantCount; private Boolean isParticipated; - public OfficialAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + public OfficialAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long timeRemaining, Long participantCount, Boolean isParticipated) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.timeRemaining = timeRemaining; this.participantCount = participantCount; this.isParticipated = isParticipated; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java index 7dfc9299..d52f9537 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionDetailResponse.java @@ -14,10 +14,10 @@ public class PreAuctionDetailResponse extends BaseAuctionDetailResponse { private Boolean isLiked; public PreAuctionDetailResponse(Long auctionId, String sellerNickname, String sellerProfileImageUrl, - String productName, + String auctionName, String description, Integer minPrice, Boolean isSeller, AuctionStatus status, Category category, LocalDateTime updatedAt, Long likeCount, Boolean isLiked) { - super(auctionId, sellerNickname, sellerProfileImageUrl, productName, description, minPrice, isSeller, status, + super(auctionId, sellerNickname, sellerProfileImageUrl, auctionName, description, minPrice, isSeller, status, category); this.updatedAt = updatedAt; this.likeCount = likeCount; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java index 2ff2c626..4bc5deaf 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/PreAuctionResponse.java @@ -9,9 +9,9 @@ public class PreAuctionResponse extends BaseAuctionResponse { private Long likeCount; private Boolean isLiked; - public PreAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + public PreAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long likeCount, Boolean isLiked) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.likeCount = likeCount; this.isLiked = isLiked; } diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java index 09f7320f..11c67587 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/ProceedingAuctionResponse.java @@ -13,11 +13,11 @@ public class ProceedingAuctionResponse extends BaseAuctionResponse { private Long participantCount; private LocalDateTime createdAt; - public ProceedingAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, + public ProceedingAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long timeRemaining, AuctionStatus status, Long participantCount, LocalDateTime createdAt) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.timeRemaining = timeRemaining; this.status = status; this.participantCount = participantCount; diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java index e281fbfd..0ba03307 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionDetailsResponse.java @@ -4,7 +4,7 @@ public record WonAuctionDetailsResponse( Long auctionId, - String productName, + String auctionName, String imageUrl, Long winningAmount ) { diff --git a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java index dad12a41..4f338c30 100644 --- a/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java +++ b/src/main/java/org/chzz/market/domain/auction/dto/response/WonAuctionResponse.java @@ -13,10 +13,10 @@ public class WonAuctionResponse extends BaseAuctionResponse { private Boolean isOrdered; private Long orderId; - public WonAuctionResponse(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + public WonAuctionResponse(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long participantCount, LocalDateTime endDateTime, Long winningAmount, Boolean isOrdered, Long orderId) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.participantCount = participantCount; this.endDateTime = endDateTime; this.winningAmount = winningAmount; diff --git a/src/main/java/org/chzz/market/domain/auction/entity/Auction.java b/src/main/java/org/chzz/market/domain/auction/entity/Auction.java index f2d9088a..48693d4c 100644 --- a/src/main/java/org/chzz/market/domain/auction/entity/Auction.java +++ b/src/main/java/org/chzz/market/domain/auction/entity/Auction.java @@ -169,7 +169,7 @@ public void assignWinner(final Long bidderId) { } public void update(final UpdateAuctionRequest request) { - this.name = request.getProductName(); + this.name = request.getAuctionName(); this.description = request.getDescription(); this.category = request.getCategory(); this.minPrice = request.getMinPrice(); diff --git a/src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java index 73b8a4d5..1ff03758 100644 --- a/src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionEndService.java @@ -42,48 +42,48 @@ public void endAuction(final Long auctionId) { */ private void notifyAuctionEnded(Auction auction) { Long sellerId = auction.getSeller().getId(); - String productName = auction.getName(); + String auctionName = auction.getName(); String firstImageCdnPath = auction.getFirstImageCdnPath(); List bids = bidRepository.findAllBidsByAuction(auction); if (bids.isEmpty()) { // 입찰이 없는 경우 eventPublisher.publishEvent( NotificationEvent.createSimpleNotification(sellerId, AUCTION_FAILURE, - AUCTION_FAILURE.getMessage(productName), + AUCTION_FAILURE.getMessage(auctionName), firstImageCdnPath)); // 낙찰 실패 알림 이벤트 return; } eventPublisher.publishEvent( NotificationEvent.createAuctionNotification(sellerId, AUCTION_SUCCESS, - AUCTION_SUCCESS.getMessage(productName), + AUCTION_SUCCESS.getMessage(auctionName), firstImageCdnPath, auction.getId())); // 낙찰 성공 알림 이벤트 - alter2Winner(auction, bids.get(0), productName, firstImageCdnPath); // 첫 번째 입찰이 낙찰 - notify2NonWinner(bids, productName, firstImageCdnPath); + alter2Winner(auction, bids.get(0), auctionName, firstImageCdnPath); // 첫 번째 입찰이 낙찰 + notify2NonWinner(bids, auctionName, firstImageCdnPath); } /** * 낙찰자에게 알림 전송 */ - private void alter2Winner(Auction auction, Bid winningBid, String productName, String firstImageCdnPath) { + private void alter2Winner(Auction auction, Bid winningBid, String auctionName, String firstImageCdnPath) { auction.assignWinner(winningBid.getBidderId()); eventPublisher.publishEvent( NotificationEvent.createAuctionNotification(winningBid.getBidderId(), AUCTION_WINNER, - AUCTION_WINNER.getMessage(productName), firstImageCdnPath, auction.getId())); // 낙찰자 알림 이벤트 + AUCTION_WINNER.getMessage(auctionName), firstImageCdnPath, auction.getId())); // 낙찰자 알림 이벤트 log.info("경매 ID {}: 낙찰자 처리 완료", auction.getId()); } /** * 미낙찰자들에게 알림 전송 */ - private void notify2NonWinner(List bids, String productName, String firstImageCdnPath) { + private void notify2NonWinner(List bids, String auctionName, String firstImageCdnPath) { List nonWinnerIds = bids.stream().skip(1) // 낙찰자를 제외한 나머지 입찰자들 .map(Bid::getBidderId).collect(Collectors.toList()); if (!nonWinnerIds.isEmpty()) { eventPublisher.publishEvent(NotificationEvent.createSimpleNotification(nonWinnerIds, AUCTION_NON_WINNER, - AUCTION_NON_WINNER.getMessage(productName), firstImageCdnPath)); // 미낙찰자 알림 이벤트 + AUCTION_NON_WINNER.getMessage(auctionName), firstImageCdnPath)); // 미낙찰자 알림 이벤트 } } } diff --git a/src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java index 910cb912..3887f8e1 100644 --- a/src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionModifyService.java @@ -1,7 +1,5 @@ package org.chzz.market.domain.auction.service; -import java.util.Collections; -import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.chzz.market.domain.auction.dto.AuctionImageUpdateEvent; @@ -11,10 +9,10 @@ import org.chzz.market.domain.auction.error.AuctionErrorCode; import org.chzz.market.domain.auction.error.AuctionException; import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.image.service.ObjectKeyValidator; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @@ -22,11 +20,11 @@ public class AuctionModifyService { private final AuctionRepository auctionRepository; private final ApplicationEventPublisher eventPublisher; + private final ObjectKeyValidator objectKeyValidator; @Transactional public UpdateAuctionResponse updateAuction(Long userId, Long auctionId, - UpdateAuctionRequest request, - Map newImages) { + UpdateAuctionRequest request) { // 경매 조회 Auction auction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionException(AuctionErrorCode.AUCTION_NOT_FOUND)); @@ -42,19 +40,15 @@ public UpdateAuctionResponse updateAuction(Long userId, Long auctionId, // 경매 정보 업데이트 auction.update(request); + request.getObjectKeyBuffer().values().forEach(objectKeyValidator::validate); + // 이미지 업데이트 이벤트 - Map imageBuffer = removeRequestKey(newImages);//request 제거 - AuctionImageUpdateEvent event = new AuctionImageUpdateEvent(auction, request, imageBuffer); + AuctionImageUpdateEvent event = new AuctionImageUpdateEvent(auction, request.getImageSequence(), + request.getObjectKeyBuffer()); eventPublisher.publishEvent(event); log.info("경매 ID {}번에 대한 사전 등록 정보를 업데이트를 완료했습니다.", auctionId); return UpdateAuctionResponse.from(auction); } - private Map removeRequestKey(Map newImages) { - if (newImages != null) { - newImages.remove("request"); - } - return newImages != null ? newImages : Collections.emptyMap(); - } } diff --git a/src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java index 3e7b56c1..e9264996 100644 --- a/src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionRegistrationService.java @@ -7,9 +7,10 @@ import org.chzz.market.domain.auction.dto.AuctionRegistrationEvent; import org.chzz.market.domain.auction.dto.ImageUploadEvent; import org.chzz.market.domain.auction.dto.request.RegisterRequest; -import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.image.service.ObjectKeyValidator; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.error.UserErrorCode; import org.chzz.market.domain.user.error.exception.UserException; @@ -17,7 +18,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @@ -26,25 +26,28 @@ public class AuctionRegistrationService implements RegistrationService { private final AuctionRepository auctionRepository; private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; + private final ObjectKeyValidator objectKeyValidator; @Override @Transactional - public void register(final Long userId, RegisterRequest request, final List images) { + public void register(final Long userId, RegisterRequest request) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); Auction auction = createAuction(request, user); auctionRepository.save(auction); + List objectKeys = request.objectKeys(); + objectKeys.forEach(objectKeyValidator::validate); - eventPublisher.publishEvent(new ImageUploadEvent(auction, images)); + eventPublisher.publishEvent(new ImageUploadEvent(auction, objectKeys)); eventPublisher.publishEvent(new AuctionRegistrationEvent(auction.getId(), auction.getEndDateTime())); } private Auction createAuction(final RegisterRequest request, final User user) { return Auction.builder() - .name(request.productName()) + .name(request.auctionName()) .minPrice(request.minPrice()) .description(request.description()) .category(request.category()) diff --git a/src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java index 241fcdc2..773f6b79 100644 --- a/src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/PreAuctionRegistrationService.java @@ -4,9 +4,10 @@ import lombok.RequiredArgsConstructor; import org.chzz.market.domain.auction.dto.ImageUploadEvent; import org.chzz.market.domain.auction.dto.request.RegisterRequest; -import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.repository.AuctionRepository; +import org.chzz.market.domain.image.service.ObjectKeyValidator; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.error.UserErrorCode; import org.chzz.market.domain.user.error.exception.UserException; @@ -14,7 +15,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor @@ -22,23 +22,26 @@ public class PreAuctionRegistrationService implements RegistrationService { private final AuctionRepository auctionRepository; private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; + private final ObjectKeyValidator objectKeyValidator; @Override @Transactional - public void register(final Long userId, RegisterRequest request, final List images) { + public void register(final Long userId, RegisterRequest request) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); Auction auction = createAuction(request, user); auctionRepository.save(auction); + List objectKeys = request.objectKeys(); + objectKeys.forEach(objectKeyValidator::validate); - eventPublisher.publishEvent(new ImageUploadEvent(auction, images)); + eventPublisher.publishEvent(new ImageUploadEvent(auction, objectKeys)); } private Auction createAuction(final RegisterRequest request, final User user) { return Auction.builder() - .name(request.productName()) + .name(request.auctionName()) .minPrice(request.minPrice()) .category(request.category()) .description(request.description()) diff --git a/src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java b/src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java index d57bc2c7..426f471b 100644 --- a/src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/RegistrationService.java @@ -1,9 +1,7 @@ package org.chzz.market.domain.auction.service; -import java.util.List; import org.chzz.market.domain.auction.dto.request.RegisterRequest; -import org.springframework.web.multipart.MultipartFile; public interface RegistrationService { - void register(Long userId, RegisterRequest request, List images); + void register(Long userId, RegisterRequest request); } diff --git a/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java b/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java index 483299d9..2f4c6e06 100644 --- a/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java +++ b/src/main/java/org/chzz/market/domain/bid/dto/response/BiddingRecord.java @@ -9,9 +9,9 @@ public class BiddingRecord extends BaseAuctionResponse { private final Long participantCount; private final Long bidAmount; - public BiddingRecord(Long auctionId, String productName, String imageUrl, Long minPrice, Boolean isSeller, + public BiddingRecord(Long auctionId, String auctionName, String imageUrl, Long minPrice, Boolean isSeller, Long timeRemaining, Long participantCount, Long bidAmount) { - super(auctionId, productName, imageUrl, minPrice, isSeller); + super(auctionId, auctionName, imageUrl, minPrice, isSeller); this.timeRemaining = timeRemaining; this.participantCount = participantCount; this.bidAmount = bidAmount; diff --git a/src/main/java/org/chzz/market/domain/image/controller/ImageApi.java b/src/main/java/org/chzz/market/domain/image/controller/ImageApi.java new file mode 100644 index 00000000..59da9150 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/image/controller/ImageApi.java @@ -0,0 +1,41 @@ +package org.chzz.market.domain.image.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.chzz.market.domain.image.dto.response.CreatePresignedUrlResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@Tag(name = "image", description = "이미지 API") +@RequestMapping("/v1/image") +public interface ImageApi { + + @Operation( + summary = "사용자 프로필 이미지 업로드를 위한 인증 url 발급", + description = """ + 사용자가 프로필 이미지를 업로드 하기 위해 S3에 업로드 권한을 url 형태로 발급 받습니다. + url의 인증 만료 시간은 2분입니다. + """ + ) + @ApiResponses(value = { + + }) + public ResponseEntity createProfileImagePresignedUrl(@RequestBody String fileName); + + @Operation( + summary = "경매 이미지들을 업로드하기 위한 인증 url 발급", + description = """ + 경매 이미지들를 업로드 하기 위해 S3에 업로드 권한을 url 형태로 발급 받습니다. + 이미지 하나당 인증 url이 하나씩 필요합니다. + url의 인증 만료 시간은 2분입니다. + """ + ) + @ApiResponses(value = { + + }) + public ResponseEntity> createAuctionPresignedUrls( + @RequestBody List requests); +} diff --git a/src/main/java/org/chzz/market/domain/image/controller/ImageController.java b/src/main/java/org/chzz/market/domain/image/controller/ImageController.java new file mode 100644 index 00000000..3ee96f93 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/image/controller/ImageController.java @@ -0,0 +1,33 @@ +package org.chzz.market.domain.image.controller; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.config.aws.BucketPrefix; +import org.chzz.market.domain.image.dto.response.CreatePresignedUrlResponse; +import org.chzz.market.domain.image.service.ImageUploadService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/image") +public class ImageController implements ImageApi { + private final ImageUploadService imageUploadService; + + @Override + @PostMapping("/profile") + public ResponseEntity createProfileImagePresignedUrl(final String fileName) { + CreatePresignedUrlResponse response = imageUploadService.createPresignedUrl(BucketPrefix.PROFILE, fileName); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @Override + @PostMapping("/auction") + public ResponseEntity> createAuctionPresignedUrls(final List requests) { + List presignedUrls = imageUploadService.createAuctionPresignedUrls(requests); + return ResponseEntity.status(HttpStatus.CREATED).body(presignedUrls); + } +} diff --git a/src/main/java/org/chzz/market/domain/image/dto/response/CreatePresignedUrlResponse.java b/src/main/java/org/chzz/market/domain/image/dto/response/CreatePresignedUrlResponse.java new file mode 100644 index 00000000..709d0e20 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/image/dto/response/CreatePresignedUrlResponse.java @@ -0,0 +1,11 @@ +package org.chzz.market.domain.image.dto.response; + +import java.util.Date; + +public record CreatePresignedUrlResponse(String objectKey, + String uploadUrl, + Date expiration) { + public static CreatePresignedUrlResponse of(final String objectKey, final String url, final Date expiration) { + return new CreatePresignedUrlResponse(objectKey, url, expiration); + } +} diff --git a/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java b/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java index adcba7d9..529a0b32 100644 --- a/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java +++ b/src/main/java/org/chzz/market/domain/image/error/ImageErrorCode.java @@ -11,7 +11,8 @@ public enum ImageErrorCode implements ErrorCode { INVALID_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 확장자입니다."), IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지가 존재하지 않습니다."), IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드를 실패했습니다."), - IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다."); + IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다."), + INVALID_OBJECT_KEY(HttpStatus.INTERNAL_SERVER_ERROR,"존재하지 않는 object key입니다."),; private final HttpStatus httpStatus; private final String message; @@ -21,5 +22,6 @@ public static class Const { public static final String IMAGE_NOT_FOUND = "IMAGE_NOT_FOUND"; public static final String IMAGE_UPLOAD_FAILED = "IMAGE_UPLOAD_FAILED"; public static final String IMAGE_DELETE_FAILED = "IMAGE_DELETE_FAILED"; + public static final String INVALID_OBJECT_KEY = "INVALID_OBJECT_KEY"; } } diff --git a/src/main/java/org/chzz/market/domain/image/service/ImageService.java b/src/main/java/org/chzz/market/domain/image/service/ImageService.java index 05ad95bd..d51aa8e4 100644 --- a/src/main/java/org/chzz/market/domain/image/service/ImageService.java +++ b/src/main/java/org/chzz/market/domain/image/service/ImageService.java @@ -1,143 +1,79 @@ package org.chzz.market.domain.image.service; -import static org.chzz.market.domain.image.error.ImageErrorCode.INVALID_IMAGE_EXTENSION; - import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.UUID; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.chzz.market.domain.auction.dto.AuctionImageUpdateEvent; import org.chzz.market.domain.auction.dto.ImageUploadEvent; -import org.chzz.market.domain.auction.dto.request.UpdateAuctionRequest; import org.chzz.market.domain.auction.entity.Auction; import org.chzz.market.domain.auction.error.AuctionErrorCode; import org.chzz.market.domain.auction.error.AuctionException; import org.chzz.market.domain.image.entity.Image; -import org.chzz.market.domain.image.error.exception.ImageException; import org.chzz.market.domain.image.repository.ImageRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @RequiredArgsConstructor public class ImageService { - private static final List ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp"); - @Value("${cloud.aws.cloudfront.domain}") private String cloudfrontDomain; private final ImageRepository imageRepository; - private final S3ImageUploader s3ImageUploader; @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void uploadImages(final ImageUploadEvent event) { - Map buffer = setImageBuffer(event); - - List paths = s3ImageUploader.uploadImages(buffer); - Auction auction = event.auction(); + List objectKeys = event.objectKeys(); - List list = createImages(auction, paths); + List images = createImages(auction, objectKeys); - auction.addImages(list); + auction.addImages(images); - imageRepository.saveAll(list); + imageRepository.saveAll(images); } @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void modifyImages(AuctionImageUpdateEvent event) { Auction auction = event.auction(); - UpdateAuctionRequest request = event.request(); - Map buffer = event.imageBuffer(); - Map sequence = Optional.ofNullable(request.getImageSequence()).orElse(Collections.emptyMap()); - updateAuctionImages(auction, sequence, buffer); + Map sequence = event.imageSequence(); + Map objectKeyBuffer = event.objectKeyBuffer(); + updateAuctionImages(auction, sequence, objectKeyBuffer); } /** - * 단일 파일 업로드 - */ - public String uploadImage(MultipartFile file) { - String uniqueFileName = createUniqueFileName(file); - return cloudfrontDomain + "/" + s3ImageUploader.uploadImage(file, uniqueFileName); - } - - /** - * key - unique한 이미지 파일명
value - 해당 파일의 {@link MultipartFile} - */ - private Map setImageBuffer(final ImageUploadEvent event) { - Map imageBuffer = new HashMap<>(); - for (MultipartFile image : event.images()) { - String uniqueFileName = createUniqueFileName(image); - imageBuffer.put(uniqueFileName, image); - } - return imageBuffer; - } - - /** - * @param paths 업로드된 이미지의 cdn 경로들 + * @param objectKeys 업로드된 이미지의 cdn 경로들 * @return cdn과 순서를 적용한 {@link Image} list */ - private List createImages(final Auction auction, final List paths) { - return IntStream.range(0, paths.size()) + private List createImages(final Auction auction, final List objectKeys) { + return IntStream.range(0, objectKeys.size()) .mapToObj(i -> Image.builder() - .cdnPath(cloudfrontDomain + "/" + paths.get(i)) + .cdnPath(cloudfrontDomain + "/" + objectKeys.get(i)) .sequence((i + 1)) .auction(auction) .build()) .toList(); } - /** - * @param file 업로드한 파일 - * @return 원본파일명을 기반으로한 unique한 파일명 - */ - private String createUniqueFileName(MultipartFile file) { - String uuid = UUID.randomUUID().toString(); - String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); - - if (extension == null || !isValidFileExtension(extension)) { - throw new ImageException(INVALID_IMAGE_EXTENSION); - } - - return uuid + "." + extension; - } - - /** - * 파일 확장자 검증기
- * - * @param extension 파일 확장자 - */ - private boolean isValidFileExtension(String extension) { - return ALLOWED_EXTENSIONS.contains(extension.toLowerCase()); - } + private void updateAuctionImages(final Auction auction, + final Map sequence, + final Map objectKeyBuffer) { + validateTotalImageCount(sequence.size() + objectKeyBuffer.size()); - /** - * 이미지 순서쌍을 포함한 변경요청({@link UpdateAuctionRequest})을 이용해 이미지 순서 변경 - */ - private void updateAuctionImages(final Auction auction, final Map sequence, - final Map multipartFileBuffer) { - validateTotalImageCount(sequence.size() + multipartFileBuffer.size()); // 기존 이미지 처리 (업데이트할 이미지와 삭제할 이미지 구분) processExistingImages(auction, sequence); // 새 이미지가 있는 경우 - if (!multipartFileBuffer.isEmpty()) { - uploadAndAddNewImages(auction, multipartFileBuffer); + if (!objectKeyBuffer.isEmpty()) { + uploadAndAddNewImages(auction, objectKeyBuffer); } auction.validateImageSize();// 업로드 이후 이미지 수량 검증 - } /** @@ -182,25 +118,20 @@ private void updateImageSequences(final List imagesToUpdate, final Map multipartFileBuffer) { - List newImageEntities = uploadSequentialImages(auction, multipartFileBuffer); + private void uploadAndAddNewImages(final Auction auction, final Map objectKeyBuffer) { + List newImageEntities = uploadSequentialImages(auction, objectKeyBuffer); auction.addImages(newImageEntities); log.info("경매 ID {}번의 새 이미지를 성공적으로 저장하였습니다.", auction.getId()); } - private List uploadSequentialImages(Auction auction, Map newImages) { - List images = newImages.entrySet().stream() + private List uploadSequentialImages(final Auction auction, final Map objectKeyBuffer) { + List images = objectKeyBuffer.entrySet().stream() .map(entry -> { int sequence = Integer.parseInt(entry.getKey()); - MultipartFile multipartFile = entry.getValue(); - String uniqueFileName = createUniqueFileName(multipartFile); - String cdnPath = s3ImageUploader.uploadImage(multipartFile, uniqueFileName); + String objectKey = entry.getValue(); return Image.builder() .sequence(sequence) - .cdnPath(cloudfrontDomain + "/" + cdnPath) + .cdnPath(cloudfrontDomain + "/" + objectKey) .auction(auction) .build(); }).toList(); diff --git a/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java b/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java new file mode 100644 index 00000000..f2d248c9 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java @@ -0,0 +1,55 @@ +package org.chzz.market.domain.image.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import java.net.URL; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.chzz.market.common.config.aws.BucketPrefix; +import org.chzz.market.domain.image.dto.response.CreatePresignedUrlResponse; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ImageUploadService { + private static final int DURATION_MILLIS = 1000 * 60 * 2; + + private final AmazonS3 amazonS3; + private final String s3BucketName; + + public CreatePresignedUrlResponse createPresignedUrl(BucketPrefix bucketPrefix, String fileName) { + Date expiration = getPreSignedUrlExpiration(); + String objectKey = bucketPrefix.createPath(fileName); + GeneratePresignedUrlRequest request = getGeneratePreSignedUrlRequest(objectKey, expiration); + URL url = amazonS3.generatePresignedUrl(request); + return CreatePresignedUrlResponse.of(objectKey, url.toString(), expiration); + } + + public List createAuctionPresignedUrls(final List requests) { + Date expiration = getPreSignedUrlExpiration(); + String fileId = UUID.randomUUID().toString();//하니의 경매가 동일한 fileId를 갖음 + String name = BucketPrefix.AUCTION.getName(); + return requests.stream() + .map(fileName -> { + String objectKey = String.format("%s/%s/%s", name, fileId, fileName.hashCode());//실제로 파일명은 해시값으로 구분 + GeneratePresignedUrlRequest request = getGeneratePreSignedUrlRequest(objectKey, expiration); + URL url = amazonS3.generatePresignedUrl(request); + return CreatePresignedUrlResponse.of(objectKey, url.toString(), expiration); + }) + .toList(); + } + + private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(final String fileName, final Date expiration) { + return new GeneratePresignedUrlRequest(s3BucketName, fileName) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + } + + private Date getPreSignedUrlExpiration() { + return Date.from(Instant.now().plusMillis(DURATION_MILLIS)); + } +} diff --git a/src/main/java/org/chzz/market/domain/image/service/ObjectKeyValidator.java b/src/main/java/org/chzz/market/domain/image/service/ObjectKeyValidator.java new file mode 100644 index 00000000..abb552f8 --- /dev/null +++ b/src/main/java/org/chzz/market/domain/image/service/ObjectKeyValidator.java @@ -0,0 +1,20 @@ +package org.chzz.market.domain.image.service; + +import com.amazonaws.services.s3.AmazonS3; +import lombok.RequiredArgsConstructor; +import org.chzz.market.domain.image.error.ImageErrorCode; +import org.chzz.market.domain.image.error.exception.ImageException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ObjectKeyValidator { + private final AmazonS3 amazonS3; + private final String bucket; + + public void validate(String objectKey) { + if (!amazonS3.doesObjectExist(bucket, objectKey)) { + throw new ImageException(ImageErrorCode.INVALID_OBJECT_KEY); + } + } +} diff --git a/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java b/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java deleted file mode 100644 index b74baf9b..00000000 --- a/src/main/java/org/chzz/market/domain/image/service/S3ImageUploader.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.chzz.market.domain.image.service; - -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.ObjectMetadata; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.image.error.ImageErrorCode; -import org.chzz.market.domain.image.error.exception.ImageException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Slf4j -@Service -@RequiredArgsConstructor -public class S3ImageUploader { - private final AmazonS3 amazonS3Client; - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - public String uploadImage(MultipartFile image, String fileName) { - try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(image.getSize()); - metadata.setContentType(image.getContentType()); - - amazonS3Client.putObject(bucket, fileName, image.getInputStream(), metadata); - - return fileName; // CDN 경로 생성 (전체 URL 아닌 경로만) - } catch (IOException e) { - throw new ImageException(ImageErrorCode.IMAGE_UPLOAD_FAILED); - } - } - - public List uploadImages(final Map multipartFiles) { - return multipartFiles.entrySet().stream() - .map(entry -> uploadImage(entry.getValue(), entry.getKey())) - .toList(); - } -} diff --git a/src/main/java/org/chzz/market/domain/notification/entity/NotificationType.java b/src/main/java/org/chzz/market/domain/notification/entity/NotificationType.java index 26071ca3..8809e8de 100644 --- a/src/main/java/org/chzz/market/domain/notification/entity/NotificationType.java +++ b/src/main/java/org/chzz/market/domain/notification/entity/NotificationType.java @@ -2,7 +2,6 @@ import lombok.AllArgsConstructor; import org.chzz.market.domain.notification.event.NotificationEvent; -import org.chzz.market.domain.user.entity.User; @AllArgsConstructor public enum NotificationType { @@ -46,8 +45,8 @@ public Notification createNotification(Long userId, NotificationEvent event) { private final String message; private String value; - public String getMessage(String productName) { - return String.format(message, productName); + public String getMessage(String auctionName) { + return String.format(message, auctionName); } public abstract Notification createNotification(Long userId, NotificationEvent event); diff --git a/src/main/java/org/chzz/market/domain/user/controller/UserApi.java b/src/main/java/org/chzz/market/domain/user/controller/UserApi.java index 95038e23..b0448bf8 100644 --- a/src/main/java/org/chzz/market/domain/user/controller/UserApi.java +++ b/src/main/java/org/chzz/market/domain/user/controller/UserApi.java @@ -38,7 +38,7 @@ ResponseEntity completeRegistration(Long userId, @Valid UserCreateRequest HttpServletResponse response); @Operation(summary = "프로필 수정") - ResponseEntity updateUserProfile(Long userId, MultipartFile file, @Valid UpdateUserProfileRequest request); + ResponseEntity updateUserProfile(Long userId, @Valid UpdateUserProfileRequest request); @Operation(summary = "JWT 토큰 재발급") ResponseEntity reissue(HttpServletRequest request, HttpServletResponse response); diff --git a/src/main/java/org/chzz/market/domain/user/controller/UserController.java b/src/main/java/org/chzz/market/domain/user/controller/UserController.java index e7a9be9c..4437bbb2 100644 --- a/src/main/java/org/chzz/market/domain/user/controller/UserController.java +++ b/src/main/java/org/chzz/market/domain/user/controller/UserController.java @@ -97,9 +97,8 @@ public ResponseEntity completeRegistration(@LoginUser Long userId, @PostMapping(value = "/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity updateUserProfile( @LoginUser Long userId, - @RequestPart(required = false) MultipartFile file, @RequestPart @Valid UpdateUserProfileRequest request) { - userService.updateUserProfile(userId, file, request); + userService.updateUserProfile(userId, request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/org/chzz/market/domain/user/dto/request/UpdateUserProfileRequest.java b/src/main/java/org/chzz/market/domain/user/dto/request/UpdateUserProfileRequest.java index 437bacea..b333138f 100644 --- a/src/main/java/org/chzz/market/domain/user/dto/request/UpdateUserProfileRequest.java +++ b/src/main/java/org/chzz/market/domain/user/dto/request/UpdateUserProfileRequest.java @@ -28,4 +28,6 @@ public class UpdateUserProfileRequest { @Builder.Default private Boolean useDefaultImage = false; + + private String objectKey; } diff --git a/src/main/java/org/chzz/market/domain/user/service/UserService.java b/src/main/java/org/chzz/market/domain/user/service/UserService.java index 89929b99..879632c0 100644 --- a/src/main/java/org/chzz/market/domain/user/service/UserService.java +++ b/src/main/java/org/chzz/market/domain/user/service/UserService.java @@ -8,7 +8,6 @@ import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.repository.AuctionQueryRepository; import org.chzz.market.domain.auction.repository.AuctionRepository; -import org.chzz.market.domain.image.service.ImageService; import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest; import org.chzz.market.domain.user.dto.request.UserCreateRequest; import org.chzz.market.domain.user.dto.response.NicknameAvailabilityResponse; @@ -17,19 +16,20 @@ import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.error.exception.UserException; import org.chzz.market.domain.user.repository.UserRepository; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserService { - private final ImageService imageService; private final UserRepository userRepository; private final AuctionRepository auctionRepository; private final AuctionQueryRepository auctionQueryRepository; + @Value("${cloud.aws.cloudfront.domain}") + private String cloudfrontDomain; /** * 사용자 프로필 조회 (유저 ID 기반) @@ -77,7 +77,7 @@ public User completeUserRegistration(Long userId, UserCreateRequest userCreateRe * 프로필 수정 */ @Transactional - public void updateUserProfile(Long userId, MultipartFile file, UpdateUserProfileRequest request) { + public void updateUserProfile(Long userId, UpdateUserProfileRequest request) { // 유저 유효성 검사 User existingUser = userRepository.findById(userId) .orElseThrow(() -> new UserException(USER_NOT_FOUND)); @@ -87,22 +87,18 @@ public void updateUserProfile(Long userId, MultipartFile file, UpdateUserProfile .ifPresent(user -> { throw new UserException(NICKNAME_DUPLICATION); }); - String profileImageUrl = handleProfileImage(file, request.getUseDefaultImage(), + String profileImageUrl = handleProfileImage(request.getObjectKey(), request.getUseDefaultImage(), existingUser.getProfileImageUrl()); - log.info("profileImageUrl = {}", profileImageUrl); - // 프로필 정보 업데이트 existingUser.updateProfile(request, profileImageUrl); } /** * 회원 프로필 이미지 변경 */ - private String handleProfileImage(MultipartFile file, Boolean useDefaultImage, String currentImageUrl) { + private String handleProfileImage(String objectKey, Boolean useDefaultImage, String currentProfileImageUrl) { if (useDefaultImage) { return null; // 기존 이미지를 삭제하고 기본이미지로 (null) - } else if (file != null && !file.isEmpty()) { - return imageService.uploadImage(file); // 새 이미지 업로드 } - return currentImageUrl; // 기존 이미지 유지 + return cloudfrontDomain + "/" + objectKey; } } diff --git a/src/main/resources/db/data.sql b/src/main/resources/db/data.sql deleted file mode 100644 index 67cae76a..00000000 --- a/src/main/resources/db/data.sql +++ /dev/null @@ -1,357 +0,0 @@ -LOCK TABLES `users` WRITE; -/*!40000 ALTER TABLE `users` DISABLE KEYS */; -INSERT INTO `users` VALUES -('2024-08-18 01:20:13.000000','2023-08-19 16:09:18.000000',1,'Quon Valdez','faucibus.orci@outlook.ca','1666439','NAVER','USER','',''), -('2025-06-27 04:17:16.000000','2024-11-06 22:09:09.000000',2,'Amethyst Shannon','leo@hotmail.net','1678792','KAKAO','USER','',''); -/*!40000 ALTER TABLE `users` ENABLE KEYS */; -UNLOCK TABLES; - -LOCK TABLES `product` WRITE; -/*!40000 ALTER TABLE `product` DISABLE KEYS */; -INSERT INTO `product` VALUES -(9000,'2024-01-21 06:37:43.000000',1,'2025-03-29 09:09:57.000000',2,'sodales elit erat vitae risus. Duis a mi fringilla mi','Alice Burks','ELECTRONICS'), -(8000,'2024-04-21 12:55:35.000000',2,'2025-04-24 16:53:14.000000',2,'lorem ac risus. Morbi metus. Vivamus euismod urna. Nullam lobortis','Talon Colon','HOME_APPLIANCES'), -(7000,'2023-08-24 23:18:23.000000',3,'2025-06-26 00:11:06.000000',2,'rhoncus. Nullam velit dui, semper et, lacinia vitae, sodales at,','Garrett Mercado','FASHION_AND_CLOTHING'), -(7000,'2025-04-29 08:24:33.000000',4,'2025-04-25 10:24:22.000000',2,'ipsum cursus vestibulum. Mauris magna. Duis dignissim tempor arcu. Vestibulum','Keane Conley','FURNITURE_AND_INTERIOR'), -(2000,'2024-12-29 12:25:06.000000',5,'2023-12-24 10:08:19.000000',2,'porttitor tellus non magna. Nam ligula elit, pretium et, rutrum','Martena Morrison','SPORTS_AND_LEISURE'), -(2000,'2025-02-15 20:03:25.000000',6,'2025-04-12 15:09:36.000000',2,'Curabitur sed tortor. Integer aliquam adipiscing lacus. Ut nec urna','Oren Ayala','SPORT'), -(2000,'2024-02-02 06:52:27.000000',7,'2025-06-17 10:17:26.000000',2,'posuere at, velit. Cras lorem lorem, luctus ut, pellentesque eget,','Reuben Hensley','TOYS_AND_HOBBIES'), -(2000,'2024-03-21 07:47:05.000000',8,'2024-11-14 20:05:45.000000',2,'diam eu dolor egestas rhoncus. Proin nisl sem, consequat nec,','Madison Levy','OTHER'), -(1000,'2023-08-04 04:50:20.000000',9,'2023-11-09 04:54:42.000000',2,'ac urna. Ut tincidunt vehicula risus. Nulla eget metus eu','Joel Grimes','DEFAULT '), -(1000,'2024-03-25 20:54:56.000000',10,'2023-08-25 02:08:09.000000',2,'sollicitudin a, malesuada id, erat. Etiam vestibulum massa rutrum magna.','Josiah Lambert','ELECTRONICS'), -(1000,'2024-03-13 15:40:57.000000',11,'2024-11-17 23:58:18.000000',2,'nisl. Quisque fringilla euismod enim. Etiam gravida molestie arcu. Sed','Reed George','HOME_APPLIANCES'), -(1000,'2024-01-01 13:12:49.000000',12,'2024-10-23 23:00:36.000000',1,'vel, venenatis vel, faucibus id, libero. Donec consectetuer mauris id','Elmo Mcdowell','FASHION_AND_CLOTHING'), -(1000,'2023-08-10 18:40:14.000000',13,'2024-01-13 19:38:30.000000',2,'congue turpis. In condimentum. Donec at arcu. Vestibulum ante ipsum','Malcolm Skinner','FURNITURE_AND_INTERIOR'), -(1000,'2023-12-26 02:28:22.000000',14,'2025-07-16 03:41:12.000000',1,'nunc risus varius orci, in consequat enim diam vel arcu.','Fredericka Weeks','SPORTS_AND_LEISURE'), -(1000,'2023-12-26 05:59:33.000000',15,'2023-09-15 19:54:56.000000',1,'erat eget ipsum. Suspendisse sagittis. Nullam vitae diam. Proin dolor.','Tara Dunlap','SPORT'), -(1000,'2024-10-27 10:19:06.000000',16,'2024-01-10 11:43:52.000000',1,'enim, sit amet ornare lectus justo eu arcu. Morbi sit','Signe Lyons','TOYS_AND_HOBBIES'), -(1000,'2025-01-24 02:05:55.000000',17,'2024-01-24 02:50:45.000000',1,'commodo at, libero. Morbi accumsan laoreet ipsum. Curabitur consequat, lectus','Indira Stanton','OTHER'), -(1000,'2025-03-08 04:57:15.000000',18,'2023-11-19 01:45:26.000000',2,'eu, eleifend nec, malesuada ut, sem. Nulla interdum. Curabitur dictum.','Willa Noel','DEFAULT '), -(1000,'2024-05-02 19:43:49.000000',19,'2023-08-30 07:24:10.000000',1,'rhoncus. Donec est. Nunc ullamcorper, velit in aliquet lobortis, nisi','Jameson Fuller','ELECTRONICS'), -(1000,'2023-09-06 20:14:25.000000',20,'2024-03-19 11:07:55.000000',2,'ac mi eleifend egestas. Sed pharetra, felis eget varius ultrices,','Noah Hopkins','HOME_APPLIANCES'), -(1000,'2025-04-26 09:58:46.000000',21,'2024-06-12 16:13:28.000000',2,'ultrices a, auctor non, feugiat nec, diam. Duis mi enim,','Eaton Winters','FASHION_AND_CLOTHING'), -(1000,'2024-10-17 02:39:25.000000',22,'2024-05-16 03:28:52.000000',1,'volutpat. Nulla facilisis. Suspendisse commodo tincidunt nibh. Phasellus nulla. Integer','Stephen Alford','FURNITURE_AND_INTERIOR'), -(1000,'2024-10-26 13:21:20.000000',23,'2024-10-27 22:16:13.000000',2,'metus vitae velit egestas lacinia. Sed congue, elit sed consequat','Daria Howell','SPORTS_AND_LEISURE'), -(1000,'2025-06-02 17:44:35.000000',24,'2023-09-16 06:16:10.000000',1,'tincidunt vehicula risus. Nulla eget metus eu erat semper rutrum.','Kuame Harper','SPORT'), -(1000,'2023-09-21 10:55:59.000000',25,'2024-03-07 15:14:01.000000',2,'velit. Aliquam nisl. Nulla eu neque pellentesque massa lobortis ultrices.','Rose Marsh','TOYS_AND_HOBBIES'), -(1000,'2023-09-04 15:28:19.000000',26,'2024-08-17 08:14:40.000000',1,'nibh enim, gravida sit amet, dapibus id, blandit at, nisi.','Wyoming Warren','OTHER'), -(1000,'2024-08-11 16:49:42.000000',27,'2023-11-10 08:52:53.000000',1,'adipiscing fringilla, porttitor vulputate, posuere vulputate, lacus. Cras interdum. Nunc','Abraham Dillon','DEFAULT '), -(1000,'2024-08-22 00:11:31.000000',28,'2025-01-25 23:33:05.000000',1,'amet lorem semper auctor. Mauris vel turpis. Aliquam adipiscing lobortis','Reese Floyd','ELECTRONICS'), -(1000,'2023-12-13 00:55:07.000000',29,'2024-10-09 15:59:01.000000',1,'tincidunt tempus risus. Donec egestas. Duis ac arcu. Nunc mauris.','Otto Powers','HOME_APPLIANCES'), -(1000,'2024-06-02 15:52:47.000000',30,'2024-02-25 08:26:43.000000',2,'gravida sit amet, dapibus id, blandit at, nisi. Cum sociis','Charde Michael','FASHION_AND_CLOTHING'), -(1000,'2023-12-06 02:58:40.000000',31,'2025-05-09 16:12:52.000000',1,'tellus. Phasellus elit pede, malesuada vel, venenatis vel, faucibus id,','Hector Garcia','FURNITURE_AND_INTERIOR'), -(1000,'2024-12-17 08:02:58.000000',32,'2025-06-27 09:01:53.000000',2,'nec quam. Curabitur vel lectus. Cum sociis natoque penatibus et','Joseph Bennett','SPORTS_AND_LEISURE'), -(1000,'2024-09-15 21:01:30.000000',33,'2024-09-19 10:43:34.000000',1,'lacus. Aliquam rutrum lorem ac risus. Morbi metus. Vivamus euismod','Germaine Guy','SPORT'), -(1000,'2025-04-24 00:21:22.000000',34,'2025-04-24 00:27:22.000000',2,'lorem fringilla ornare placerat, orci lacus vestibulum lorem, sit amet','Kevyn Hansen','TOYS_AND_HOBBIES'), -(1000,'2024-07-10 20:15:54.000000',35,'2024-09-11 15:53:51.000000',1,'velit in aliquet lobortis, nisi nibh lacinia orci, consectetuer euismod','Kiayada Hart','OTHER'), -(1000,'2025-07-03 01:23:24.000000',36,'2025-01-03 23:52:26.000000',1,'et, lacinia vitae, sodales at, velit. Pellentesque ultricies dignissim lacus.','Keane Paul','DEFAULT '), -(1000,'2025-01-17 23:45:34.000000',37,'2024-01-17 23:17:34.000000',2,'adipiscing lobortis risus. In mi pede, nonummy ut, molestie in,','Quamar Dejesus','ELECTRONICS'), -(1000,'2025-07-05 16:05:48.000000',38,'2025-01-28 03:53:23.000000',1,'odio sagittis semper. Nam tempor diam dictum sapien. Aenean massa.','Malcolm Hicks','HOME_APPLIANCES'), -(1000,'2025-01-30 21:55:06.000000',39,'2025-07-09 10:39:54.000000',2,'Nam tempor diam dictum sapien. Aenean massa. Integer vitae nibh.','Adam Barnett','FASHION_AND_CLOTHING'), -(1000,'2024-12-06 06:16:27.000000',40,'2025-07-02 21:21:40.000000',1,'sollicitudin adipiscing ligula. Aenean gravida nunc sed pede. Cum sociis','Kiara Cameron','FURNITURE_AND_INTERIOR'), -(1000,'2023-11-02 14:34:16.000000',41,'2023-09-03 14:39:53.000000',1,'semper et, lacinia vitae, sodales at, velit. Pellentesque ultricies dignissim','Remedios Hayden','SPORTS_AND_LEISURE'), -(1000,'2024-10-25 17:44:20.000000',42,'2025-03-03 07:34:29.000000',1,'et, lacinia vitae, sodales at, velit. Pellentesque ultricies dignissim lacus.','Rina Foley','SPORT'), -(1000,'2025-02-10 13:54:14.000000',43,'2024-02-08 05:05:23.000000',2,'nisi dictum augue malesuada malesuada. Integer id magna et ipsum','Hayfa Villarreal','TOYS_AND_HOBBIES'), -(1000,'2023-08-14 18:27:10.000000',44,'2024-06-12 04:04:49.000000',1,'et, rutrum non, hendrerit id, ante. Nunc mauris sapien, cursus','Fiona Meyer','OTHER'), -(1000,'2025-07-14 20:07:13.000000',45,'2023-10-17 21:41:06.000000',2,'tempor augue ac ipsum. Phasellus vitae mauris sit amet lorem','Echo Barlow','DEFAULT '), -(1000,'2025-03-22 08:58:40.000000',46,'2023-12-31 09:18:37.000000',1,'Sed nulla ante, iaculis nec, eleifend non, dapibus rutrum, justo.','Melinda Castro','ELECTRONICS'), -(1000,'2025-07-22 07:57:33.000000',47,'2025-05-04 15:42:09.000000',1,'eu turpis. Nulla aliquet. Proin velit. Sed malesuada augue ut','Isabella Buckley','HOME_APPLIANCES'), -(1000,'2024-12-23 03:18:22.000000',48,'2023-09-17 15:57:57.000000',2,'tempor augue ac ipsum. Phasellus vitae mauris sit amet lorem','Yoshi Miller','FASHION_AND_CLOTHING'), -(1000,'2023-12-27 23:50:27.000000',49,'2025-06-17 21:22:18.000000',2,'eu nibh vulputate mauris sagittis placerat. Cras dictum ultricies ligula.','Tanisha Christian','FURNITURE_AND_INTERIOR'), -(1000,'2024-02-22 04:51:57.000000',50,'2024-06-21 07:33:22.000000',2,'tristique ac, eleifend vitae, erat. Vivamus nisi. Mauris nulla. Integer','Rahim Neal','SPORTS_AND_LEISURE'); -/*!40000 ALTER TABLE `product` ENABLE KEYS */; -UNLOCK TABLES; - -LOCK TABLES `delivery` WRITE; -/*!40000 ALTER TABLE `delivery` DISABLE KEYS */; -INSERT INTO `delivery` VALUES (1,'2025-02-19 17:20:15.000000','2024-03-22 22:40:44.000000',1,'Ap #538-5488 Vitae, St.','Ap #668-1430 Neque. Av.','4315 Velit. St.','Ap #939-7613 Est. Road','Ap #942-3891 Eget, Road','3381'),(2,'2025-04-07 06:46:07.000000','2023-08-04 02:57:44.000000',1,'Ap #274-5005 Arcu. Rd.','363-7728 Sollicitudin St.','Ap #200-4259 Libero Rd.','296-2475 Sed Ave','Ap #833-1133 Lectus Street','1230'),(3,'2024-05-26 00:24:39.000000','2023-11-12 12:38:08.000000',2,'Ap #795-7541 Mauris Rd.','869-2832 Sed Rd.','Ap #852-2756 Ante Road','9896 Curabitur St.','P.O. Box 248, 5378 Diam St.','541848'),(4,'2025-01-09 01:21:28.000000','2024-04-18 06:55:17.000000',2,'Ap #541-2447 Nec Road','P.O. Box 614, 9429 Volutpat. Avenue','Ap #464-4402 Posuere Ave','752-9148 Volutpat. Avenue','654-6989 Nulla. Rd.','85-932'),(5,'2024-11-09 16:06:33.000000','2024-04-28 16:11:38.000000',1,'Ap #942-8059 Enim Avenue','Ap #916-2700 Diam. Ave','411-4946 Tellus. Av.','495 Enim Av.','263-647 Sit St.','66243-258'); -/*!40000 ALTER TABLE `delivery` ENABLE KEYS */; -UNLOCK TABLES; - -LOCK TABLES `auction` WRITE; -/*!40000 ALTER TABLE `auction` DISABLE KEYS */; -INSERT INTO `auction` ( `auction_id`, `created_at`, `end_date_time`, `product_id`, `updated_at`, `winner_id`, `status`) VALUES -(1, '2025-04-24 12:02:27.000000', '2025-04-25 12:02:27.000000', 13, '2025-02-11 05:07:17.000000', 1, 'PROCEEDING'), -(2, '2023-11-23 12:54:59.000000', '2023-11-24 12:54:59.000000', 26, '2025-05-01 02:30:11.000000', 1, 'COMPLETE'), -(3, '2023-09-08 19:01:35.000000', '2023-09-09 19:01:35.000000', 38, '2025-02-27 15:40:58.000000', 2, 'CANCEL'), -(4, '2025-06-08 23:59:32.000000', '2025-06-09 23:59:32.000000', 7, '2024-08-09 03:28:11.000000', 2, 'DEFAULT'), -(5, '2024-04-13 19:38:59.000000', '2024-04-14 19:38:59.000000', 24, '2024-12-22 16:41:53.000000', 2, 'PROCEEDING'), -(6, '2025-06-10 14:12:52.000000', '2025-06-11 14:12:52.000000', 45, '2025-08-02 01:23:46.000000', 2, 'COMPLETE'), -(7, '2025-06-27 02:33:25.000000', '2025-06-28 02:33:25.000000', 41, '2024-05-30 18:40:12.000000', 2, 'CANCEL'), -(8, '2024-07-11 06:23:55.000000', '2024-07-12 06:23:55.000000', 33, '2024-01-26 23:40:54.000000', 2, 'DEFAULT'), -(9, '2024-05-06 11:43:00.000000', '2024-01-24 02:43:00.000000', 17, '2025-07-29 15:27:47.000000', 2, 'PROCEEDING'), -(10, '2025-03-03 13:11:17.000000', '2025-03-04 13:11:17.000000', 31, '2025-05-01 21:45:06.000000', 1, 'COMPLETE'), -(11, '2024-11-22 03:17:24.000000', '2024-11-23 03:17:24.000000', 30, '2025-03-25 08:59:10.000000', 1, 'CANCEL'), -(12, '2023-09-07 11:42:24.000000', '2023-09-08 11:42:24.000000', 8, '2024-08-18 06:36:55.000000', 1, 'DEFAULT'), -(13, '2024-10-28 13:07:44.000000', '2024-10-29 13:07:44.000000', 46, '2025-05-18 17:23:52.000000', 1, 'PROCEEDING'), -(14, '2023-09-21 02:14:24.000000', '2023-09-22 02:14:24.000000', 49, '2025-07-20 05:35:23.000000', 2, 'COMPLETE'), -(15, '2024-12-11 04:55:06.000000', '2024-12-12 04:55:06.000000', 11, '2025-05-24 04:44:00.000000', 2, 'CANCEL'), -(16, '2025-05-16 06:39:58.000000', '2025-05-17 06:39:58.000000', 12, '2025-01-12 19:23:18.000000', 2, 'DEFAULT'), -(17, '2025-04-06 12:21:08.000000', '2025-04-07 12:21:08.000000', 37, '2023-12-02 15:54:28.000000', 1, 'PROCEEDING'), -(18, '2024-05-29 12:35:08.000000', '2024-05-30 12:35:08.000000', 50, '2023-08-09 13:51:54.000000', 1, 'COMPLETE'), -(19, '2025-06-30 20:06:38.000000', '2025-07-01 20:06:38.000000', 48, '2024-11-30 14:56:55.000000', 1, 'CANCEL'), -(20, '2024-04-26 03:39:39.000000', '2024-04-27 03:39:39.000000', 23, '2024-07-14 07:48:53.000000', 2, 'DEFAULT'), -(21, '2024-06-19 02:03:10.000000', '2024-06-20 02:03:10.000000', 4, '2024-05-27 21:23:02.000000', 1, 'PROCEEDING'), -(22, '2023-12-31 14:04:00.000000', '2024-01-01 14:04:00.000000', 1, '2023-10-02 05:11:59.000000', 2, 'COMPLETE'), -(23, '2024-07-07 19:56:15.000000', '2024-07-08 19:56:15.000000', 27, '2024-09-27 00:55:48.000000', 1, 'CANCEL'), -(24, '2023-12-10 20:00:57.000000', '2023-12-11 20:00:57.000000', 43, '2024-04-19 21:02:22.000000', 1, 'DEFAULT'), -(25, '2025-03-30 23:46:00.000000', '2025-03-31 23:46:00.000000', 44, '2025-02-20 18:22:27.000000', 1, 'PROCEEDING'), -(26, '2024-05-02 13:57:03.000000', '2024-05-03 13:57:03.000000', 22, '2023-09-29 11:44:06.000000', 1, 'COMPLETE'), -(27, '2024-06-26 20:50:47.000000', '2024-06-27 20:50:47.000000', 39, '2024-01-11 14:20:22.000000', 1, 'CANCEL'), -(28, '2023-10-03 11:12:11.000000', '2023-10-04 11:12:11.000000', 9, '2025-02-09 00:50:26.000000', 1, 'DEFAULT'), -(29, '2024-01-28 01:45:54.000000', '2024-01-29 01:45:54.000000', 42, '2024-08-18 13:06:56.000000', 1, 'PROCEEDING'), -(30, '2024-11-21 21:39:16.000000', '2024-11-22 21:39:16.000000', 19, '2024-04-16 16:24:34.000000', 2, 'COMPLETE'), -(31, '2023-10-04 09:51:23.000000', '2023-10-05 09:51:23.000000', 29, '2024-01-06 19:28:01.000000', 2, 'CANCEL'), -(32, '2023-08-12 16:29:40.000000', '2023-08-13 16:29:40.000000', 16, '2024-09-11 18:51:36.000000', 1, 'DEFAULT'), -(33, '2024-11-07 04:00:07.000000', '2024-11-08 04:00:07.000000', 5, '2025-05-09 19:20:44.000000', 1, 'PROCEEDING'), -(34, '2025-08-01 20:26:00.000000', '2025-08-02 20:26:00.000000', 3, '2024-08-02 08:53:24.000000', 2, 'COMPLETE'), -(35, '2025-06-23 00:08:27.000000', '2025-06-24 00:08:27.000000', 40, '2024-02-16 05:51:01.000000', 2, 'CANCEL'), -(36, '2023-10-22 11:01:26.000000', '2023-10-23 11:01:26.000000', 21, '2024-12-17 08:15:41.000000', 2, 'DEFAULT'), -(37, '2024-07-23 17:15:08.000000', '2024-07-24 17:15:08.000000', 2, '2025-07-24 13:45:24.000000', 2, 'PROCEEDING'), -(38, '2023-11-01 08:42:07.000000', '2023-11-02 08:42:07.000000', 6, '2023-08-23 15:21:47.000000', 2, 'COMPLETE'), -(39, '2025-05-07 06:30:56.000000', '2025-05-08 06:30:56.000000', 25, '2023-12-30 10:36:36.000000', 2, 'CANCEL'), -(40, '2025-05-25 07:29:46.000000', '2025-05-26 07:29:46.000000', 10, '2024-08-21 22:00:24.000000', 2, 'DEFAULT'), -(41, '2025-07-15 18:56:52.000000', '2025-07-16 18:56:52.000000', 14, '2023-11-04 00:27:06.000000', 1, 'PROCEEDING'), -(42, '2024-09-18 09:36:34.000000', '2024-09-19 09:36:34.000000', 15, '2024-12-09 18:04:01.000000', 1, 'COMPLETE'), -(43, '2025-01-09 14:47:16.000000', '2025-01-10 14:47:16.000000', 18, '2025-05-23 03:02:05.000000', 1, 'CANCEL'), -(44, '2024-03-16 19:29:53.000000', '2024-03-17 19:29:53.000000', 28, '2025-07-28 04:49:28.000000', 2, 'DEFAULT'), -(45, '2024-09-05 19:37:37.000000', '2024-09-06 19:37:37.000000', 34, '2025-06-16 00:04:17.000000', 2, 'PROCEEDING'), -(46, '2025-01-17 15:02:44.000000', '2025-01-18 15:02:44.000000', 20, '2024-06-05 23:39:04.000000', 1, 'COMPLETE'), -(47, '2023-12-20 16:37:01.000000', '2023-12-21 16:37:01.000000', 47, '2025-06-25 18:45:46.000000', 1, 'CANCEL'), -(48, '2024-06-04 06:11:01.000000', '2024-06-05 06:11:01.000000', 36, '2025-05-13 01:03:20.000000', 1, 'DEFAULT'), -(49, '2025-01-15 06:01:03.000000', '2025-01-16 06:01:03.000000', 35, '2024-08-04 07:43:09.000000', 2, 'PROCEEDING'), -(50, '2023-09-13 08:55:42.000000', '2023-09-14 08:55:42.000000', 32, '2023-11-06 05:44:07.000000', 1, 'COMPLETE'); -/*!40000 ALTER TABLE `auction` ENABLE KEYS */; -UNLOCK TABLES; - -LOCK TABLES `bank_account` WRITE; -/*!40000 ALTER TABLE `bank_account` DISABLE KEYS */; -/*!40000 ALTER TABLE `bank_account` ENABLE KEYS */; -UNLOCK TABLES; - - -LOCK TABLES `bid` WRITE; -/*!40000 ALTER TABLE `bid` DISABLE KEYS */; -INSERT INTO `bid` (`count`, `amount`, `auction_id`, `bid_id`, `created_at`, `updated_at`, `user_id`, `status`) VALUES -(3, 10180, 35, 1, '2025-05-04 08:22:02.000000', '2025-03-18 07:20:12.000000', 2, 'ACTIVE'), -(3, 55126, 42, 2, '2024-06-09 22:55:08.000000', '2023-11-17 07:47:43.000000', 2, 'CANCELLED'), -(3, 67529, 40, 3, '2023-10-18 07:13:54.000000', '2024-01-04 02:28:02.000000', 1, 'ACTIVE'), -(2, 44203, 19, 4, '2023-11-01 05:03:51.000000', '2025-04-12 21:40:20.000000', 2, 'CANCELLED'), -(2, 69096, 38, 5, '2024-06-10 01:50:22.000000', '2025-08-02 03:29:27.000000', 2, 'ACTIVE'), -(1, 59032, 41, 6, '2023-11-10 14:17:05.000000', '2023-12-07 21:17:05.000000', 2, 'CANCELLED'), -(1, 50449, 41, 7, '2024-12-06 14:13:53.000000', '2025-03-08 06:28:24.000000', 2, 'ACTIVE'), -(2, 71847, 11, 8, '2024-11-11 03:14:22.000000', '2023-11-06 07:31:59.000000', 2, 'CANCELLED'), -(3, 22392, 12, 9, '2024-10-31 18:16:22.000000', '2025-03-31 21:23:52.000000', 1, 'ACTIVE'), -(3, 30459, 47, 10, '2023-10-27 22:15:33.000000', '2025-07-27 01:52:13.000000', 2, 'CANCELLED'), -(2, 27937, 24, 11, '2025-05-19 07:47:04.000000', '2024-12-05 15:48:42.000000', 1, 'ACTIVE'), -(1, 7353, 33, 12, '2024-10-19 10:15:23.000000', '2024-08-24 08:20:36.000000', 1, 'CANCELLED'), -(1, 35547, 40, 13, '2024-07-04 04:31:31.000000', '2023-08-15 09:37:06.000000', 2, 'ACTIVE'), -(2, 93548, 4, 14, '2024-05-24 19:00:38.000000', '2024-02-26 08:10:01.000000', 1, 'CANCELLED'), -(1, 52474, 15, 15, '2024-06-26 03:11:13.000000', '2024-05-07 17:15:53.000000', 2, 'ACTIVE'), -(3, 96958, 23, 16, '2025-07-05 12:56:03.000000', '2025-06-02 08:22:36.000000', 1, 'CANCELLED'), -(3, 99720, 27, 17, '2024-07-28 05:43:58.000000', '2023-12-12 17:45:10.000000', 2, 'ACTIVE'), -(3, 54194, 8, 18, '2023-11-08 08:29:50.000000', '2024-05-21 15:01:42.000000', 1, 'CANCELLED'), -(2, 41917, 40, 19, '2024-10-31 16:30:40.000000', '2024-05-05 18:15:01.000000', 1, 'ACTIVE'), -(2, 39288, 40, 20, '2023-12-27 19:36:30.000000', '2024-05-09 09:06:20.000000', 1, 'CANCELLED'), -(1, 4123, 5, 21, '2025-06-26 13:46:56.000000', '2024-03-06 16:43:38.000000', 2, 'ACTIVE'), -(3, 40227, 28, 22, '2024-10-13 05:06:45.000000', '2023-12-03 14:20:37.000000', 2, 'CANCELLED'), -(3, 21482, 39, 23, '2023-12-11 20:43:17.000000', '2024-04-20 20:12:33.000000', 2, 'ACTIVE'), -(2, 69407, 29, 24, '2023-11-10 01:30:12.000000', '2024-09-13 08:01:14.000000', 1, 'CANCELLED'), -(2, 48915, 11, 25, '2024-07-18 00:37:07.000000', '2024-11-29 20:29:38.000000', 1, 'ACTIVE'), -(2, 94095, 33, 26, '2025-05-03 06:15:39.000000', '2024-09-22 12:18:53.000000', 1, 'CANCELLED'), -(3, 79208, 27, 27, '2024-04-19 01:33:00.000000', '2023-12-17 05:28:46.000000', 1, 'ACTIVE'), -(3, 86962, 44, 28, '2024-09-06 22:26:32.000000', '2023-11-06 15:12:32.000000', 1, 'CANCELLED'), -(1, 13000, 24, 29, '2025-07-14 05:59:09.000000', '2025-04-10 15:24:01.000000', 2, 'ACTIVE'), -(1, 93139, 11, 30, '2023-12-15 01:40:32.000000', '2024-06-14 18:43:53.000000', 1, 'CANCELLED'), -(1, 72292, 38, 31, '2024-03-04 17:42:08.000000', '2024-12-11 14:04:23.000000', 1, 'ACTIVE'), -(2, 49190, 9, 32, '2023-08-09 09:06:15.000000', '2024-09-13 11:15:23.000000', 1, 'CANCELLED'), -(2, 47880, 9, 33, '2024-06-02 09:34:10.000000', '2024-12-22 13:47:11.000000', 1, 'ACTIVE'), -(2, 46955, 47, 34, '2025-06-10 20:04:23.000000', '2025-08-02 03:17:57.000000', 2, 'CANCELLED'), -(2, 84222, 25, 35, '2024-03-30 14:51:33.000000', '2025-04-03 09:52:30.000000', 2, 'ACTIVE'), -(3, 9913, 40, 36, '2024-03-05 13:41:59.000000', '2025-07-05 18:00:10.000000', 1, 'CANCELLED'), -(2, 26171, 42, 37, '2024-10-09 01:28:25.000000', '2023-10-02 05:11:22.000000', 1, 'ACTIVE'), -(1, 33305, 46, 38, '2023-11-03 05:23:28.000000', '2024-01-01 21:49:58.000000', 1, 'CANCELLED'), -(1, 36332, 42, 39, '2023-11-20 12:49:46.000000', '2025-05-15 09:39:01.000000', 1, 'ACTIVE'), -(3, 49925, 3, 40, '2024-09-19 12:40:21.000000', '2024-05-23 00:43:35.000000', 1, 'CANCELLED'), -(2, 61050, 45, 41, '2025-07-03 09:56:23.000000', '2024-08-15 17:07:46.000000', 2, 'ACTIVE'), -(2, 3376, 40, 42, '2025-07-22 00:59:00.000000', '2023-10-20 17:33:25.000000', 2, 'CANCELLED'), -(3, 99685, 48, 43, '2024-11-27 04:17:15.000000', '2025-05-09 21:18:39.000000', 2, 'ACTIVE'), -(1, 37551, 38, 44, '2023-11-27 14:07:31.000000', '2023-09-17 00:17:53.000000', 1, 'CANCELLED'), -(1, 37915, 23, 45, '2024-11-08 11:04:51.000000', '2025-01-26 19:26:56.000000', 2, 'ACTIVE'), -(1, 10107, 32, 46, '2025-02-28 22:14:31.000000', '2024-11-10 18:32:37.000000', 1, 'CANCELLED'), -(3, 81049, 17, 47, '2024-04-13 18:54:26.000000', '2024-08-22 10:04:06.000000', 1, 'ACTIVE'), -(2, 59187, 34, 48, '2024-10-31 15:37:54.000000', '2023-12-06 06:32:01.000000', 1, 'CANCELLED'), -(2, 87531, 29, 49, '2024-08-28 07:37:21.000000', '2025-06-15 15:25:44.000000', 2, 'ACTIVE'), -(3, 87489, 30, 50, '2025-02-01 23:41:36.000000', '2023-09-03 05:47:07.000000', 1, 'CANCELLED'), -(1, 74532, 49, 51, '2024-04-15 22:33:19.000000', '2024-12-23 16:19:55.000000', 2, 'ACTIVE'), -(2, 94192, 33, 52, '2025-04-18 05:53:26.000000', '2025-03-30 15:45:40.000000', 1, 'CANCELLED'), -(3, 36419, 8, 53, '2024-08-22 20:47:20.000000', '2025-06-04 06:45:53.000000', 2, 'ACTIVE'), -(2, 58926, 12, 54, '2025-02-28 01:35:58.000000', '2023-09-14 17:03:16.000000', 2, 'CANCELLED'), -(2, 91893, 21, 55, '2024-08-24 09:18:31.000000', '2024-09-02 07:55:12.000000', 1, 'ACTIVE'), -(1, 61010, 24, 56, '2024-08-18 01:33:01.000000', '2023-11-20 17:31:14.000000', 2, 'CANCELLED'), -(1, 25616, 44, 57, '2025-04-30 07:25:25.000000', '2024-12-18 14:22:05.000000', 2, 'ACTIVE'), -(2, 94031, 28, 58, '2024-07-03 20:40:02.000000', '2024-06-12 05:16:47.000000', 2, 'CANCELLED'), -(3, 51400, 13, 59, '2024-02-06 02:47:59.000000', '2025-04-05 09:47:55.000000', 2, 'ACTIVE'), -(3, 33947, 16, 60, '2023-12-13 18:03:25.000000', '2024-03-26 08:33:54.000000', 2, 'CANCELLED'), -(1, 53619, 11, 61, '2024-08-19 06:54:00.000000', '2025-04-24 22:09:40.000000', 1, 'ACTIVE'), -(2, 73593, 13, 62, '2024-05-04 05:34:13.000000', '2024-06-09 07:02:39.000000', 2, 'CANCELLED'), -(1, 39759, 6, 63, '2024-12-30 10:34:14.000000', '2024-04-24 23:03:05.000000', 2, 'ACTIVE'), -(3, 89543, 41, 64, '2023-10-28 02:30:29.000000', '2024-03-10 23:52:10.000000', 2, 'CANCELLED'), -(2, 95323, 12, 65, '2023-12-19 01:46:40.000000', '2024-03-02 15:13:24.000000', 1, 'ACTIVE'), -(2, 17647, 10, 66, '2023-11-21 19:48:36.000000', '2025-05-08 14:44:28.000000', 2, 'CANCELLED'), -(3, 97572, 3, 67, '2025-01-22 02:57:26.000000', '2025-05-11 09:58:12.000000', 2, 'ACTIVE'), -(3, 12651, 20, 68, '2025-03-28 12:49:28.000000', '2025-07-18 19:24:41.000000', 2, 'CANCELLED'), -(3, 74122, 7, 69, '2024-01-01 20:12:36.000000', '2025-02-03 15:36:09.000000', 1, 'ACTIVE'), -(2, 69925, 31, 70, '2024-08-05 06:30:53.000000', '2023-08-12 00:06:08.000000', 1, 'CANCELLED'), -(1, 81726, 15, 71, '2024-10-16 17:33:33.000000', '2024-12-01 17:56:31.000000', 1, 'ACTIVE'), -(1, 44337, 17, 72, '2024-09-25 03:15:56.000000', '2023-08-23 09:58:34.000000', 1, 'CANCELLED'), -(2, 21938, 17, 73, '2024-07-05 19:42:39.000000', '2025-06-14 11:42:51.000000', 1, 'ACTIVE'), -(3, 59849, 6, 74, '2024-02-20 11:34:40.000000', '2023-08-14 11:46:00.000000', 2, 'CANCELLED'), -(2, 79336, 27, 75, '2024-12-21 02:29:21.000000', '2024-02-19 07:53:01.000000', 1, 'ACTIVE'), -(2, 53140, 5, 76, '2023-11-13 15:28:10.000000', '2025-02-14 19:17:36.000000', 2, 'CANCELLED'), -(3, 96026, 12, 77, '2024-03-07 05:05:05.000000', '2025-02-05 23:09:10.000000', 2, 'ACTIVE'), -(1, 26453, 31, 78, '2024-08-28 01:52:07.000000', '2024-11-18 21:48:36.000000', 2, 'CANCELLED'), -(2, 7888, 22, 79, '2025-04-05 12:57:16.000000', '2024-01-08 21:51:10.000000', 1, 'ACTIVE'), -(1, 10429, 5, 80, '2024-10-23 02:19:07.000000', '2023-12-29 06:46:30.000000', 2, 'CANCELLED'), -(2, 40528, 26, 81, '2023-11-02 17:47:16.000000', '2024-10-23 20:23:33.000000', 1, 'ACTIVE'), -(3, 97389, 25, 82, '2025-06-03 21:53:39.000000', '2025-01-14 12:07:59.000000', 2, 'CANCELLED'), -(2, 17546, 8, 83, '2025-02-20 10:31:40.000000', '2023-11-06 14:10:01.000000', 1, 'ACTIVE'), -(3, 64321, 16, 84, '2023-10-06 20:50:55.000000', '2024-10-22 22:39:40.000000', 1, 'CANCELLED'), -(3, 32880, 48, 85, '2023-12-06 13:56:40.000000', '2024-03-28 01:36:41.000000', 2, 'ACTIVE'), -(3, 68114, 3, 86, '2024-08-16 19:14:40.000000', '2023-10-28 01:07:01.000000', 2, 'CANCELLED'), -(1, 49836, 8, 87, '2025-06-28 07:30:41.000000', '2025-05-21 13:47:13.000000', 2, 'ACTIVE'), -(3, 9522, 21, 88, '2024-10-31 20:49:53.000000', '2025-04-23 06:06:05.000000', 2, 'CANCELLED'), -(1, 54802, 17, 89, '2023-11-27 18:46:28.000000', '2025-07-22 18:42:47.000000', 1, 'ACTIVE'), -(3, 64234, 47, 90, '2025-07-06 06:40:41.000000', '2024-09-11 22:37:49.000000', 2, 'CANCELLED'), -(2, 63350, 39, 91, '2024-07-28 13:37:00.000000', '2025-05-21 12:09:05.000000', 2, 'ACTIVE'), -(1, 37527, 25, 92, '2023-12-22 07:01:08.000000', '2024-09-10 01:43:54.000000', 2, 'CANCELLED'), -(2, 62929, 41, 93, '2024-02-26 06:29:36.000000', '2025-07-07 03:03:48.000000', 2, 'ACTIVE'), -(2, 59151, 37, 94, '2024-01-20 00:11:16.000000', '2025-05-10 12:21:22.000000', 2, 'CANCELLED'), -(2, 58971, 15, 95, '2025-07-16 07:05:38.000000', '2025-01-14 01:14:34.000000', 2, 'ACTIVE'), -(2, 89003, 13, 96, '2024-09-14 16:29:14.000000', '2024-10-13 21:27:29.000000', 2, 'CANCELLED'), -(2, 6218, 1, 97, '2024-12-30 23:46:27.000000', '2025-05-11 20:05:39.000000', 2, 'ACTIVE'), -(2, 72731, 49, 98, '2024-06-19 12:39:21.000000', '2023-08-03 22:54:33.000000', 1, 'CANCELLED'), -(3, 77331, 6, 99, '2025-05-12 03:47:26.000000', '2023-09-21 23:24:52.000000', 1, 'ACTIVE'), -(2, 86627, 4, 100, '2024-11-18 16:05:35.000000', '2024-06-04 20:37:38.000000', 2, 'CANCELLED'), -(2, 55703, 49, 101, '2025-02-05 04:32:42.000000', '2023-11-07 08:22:31.000000', 1, 'ACTIVE'), -(2, 53509, 6, 102, '2024-08-17 18:51:04.000000', '2025-02-16 20:54:49.000000', 1, 'CANCELLED'), -(2, 14529, 16, 103, '2025-02-18 18:58:22.000000', '2024-10-30 13:52:06.000000', 1, 'ACTIVE'), -(1, 59739, 3, 104, '2024-07-18 15:36:53.000000', '2025-03-26 21:45:52.000000', 1, 'CANCELLED'), -(1, 65515, 36, 105, '2024-07-20 03:38:49.000000', '2024-03-14 17:11:51.000000', 2, 'ACTIVE'), -(2, 15876, 48, 106, '2023-11-11 09:26:34.000000', '2024-05-18 12:45:12.000000', 1, 'CANCELLED'), -(1, 73077, 8, 107, '2023-11-22 17:22:36.000000', '2025-07-11 18:10:59.000000', 2, 'ACTIVE'), -(3, 1855, 23, 108, '2023-10-02 20:59:27.000000', '2024-10-28 04:20:30.000000', 2, 'CANCELLED'), -(2, 74615, 37, 109, '2024-09-12 01:35:49.000000', '2024-09-16 21:33:01.000000', 2, 'ACTIVE'), -(1, 39333, 8, 110, '2023-08-12 04:55:06.000000', '2024-06-02 20:12:10.000000', 2, 'CANCELLED'), -(2, 70330, 9, 111, '2025-03-07 07:39:09.000000', '2024-08-08 03:40:22.000000', 1, 'ACTIVE'), -(2, 46792, 49, 112, '2024-09-26 11:55:02.000000', '2024-11-18 13:49:51.000000', 2, 'CANCELLED'), -(3, 22492, 47, 113, '2025-03-30 05:27:02.000000', '2024-09-14 06:21:10.000000', 2, 'ACTIVE'), -(3, 73974, 19, 114, '2024-01-13 06:15:22.000000', '2025-06-17 11:51:10.000000', 1, 'CANCELLED'), -(2, 49788, 21, 115, '2023-12-08 09:42:06.000000', '2024-08-01 11:48:24.000000', 2, 'ACTIVE'), -(2, 93944, 7, 116, '2024-10-26 09:41:40.000000', '2025-02-10 07:19:39.000000', 1, 'CANCELLED'), -(3, 28006, 16, 117, '2025-06-25 23:18:12.000000', '2024-08-23 23:29:02.000000', 2, 'ACTIVE'), -(1, 23100, 26, 118, '2024-07-12 00:32:01.000000', '2025-04-25 04:38:18.000000', 2, 'CANCELLED'), -(1, 33175, 6, 119, '2025-01-05 15:57:15.000000', '2024-03-25 15:36:10.000000', 1, 'ACTIVE'), -(1, 59674, 14, 120, '2024-08-25 16:42:26.000000', '2024-10-08 02:07:24.000000', 1, 'CANCELLED'), -(3, 46688, 7, 121, '2024-04-07 20:36:06.000000', '2024-09-30 11:27:51.000000', 1, 'ACTIVE'), -(2, 72453, 48, 122, '2024-03-19 21:15:25.000000', '2024-12-29 13:35:50.000000', 2, 'CANCELLED'), -(1, 75902, 16, 123, '2025-07-22 14:40:59.000000', '2024-01-19 23:08:01.000000', 1, 'ACTIVE'), -(2, 46889, 15, 124, '2024-09-03 17:24:52.000000', '2024-01-05 20:29:50.000000', 2, 'CANCELLED'), -(2, 66939, 1, 125, '2024-09-26 05:07:22.000000', '2025-05-14 19:51:44.000000', 2, 'ACTIVE'), -(1, 46564, 35, 126, '2024-04-10 21:11:09.000000', '2024-02-20 04:51:07.000000', 2, 'CANCELLED'), -(1, 94859, 36, 127, '2024-09-12 03:27:04.000000', '2024-11-03 13:37:23.000000', 1, 'ACTIVE'), -(2, 56362, 11, 128, '2024-07-17 05:06:09.000000', '2025-01-24 14:03:44.000000', 2, 'CANCELLED'), -(2, 77764, 45, 129, '2024-06-10 11:44:04.000000', '2024-10-16 06:35:58.000000', 1, 'ACTIVE'), -(2, 32518, 24, 130, '2025-05-05 19:50:55.000000', '2023-08-03 04:46:35.000000', 1, 'CANCELLED'), -(1, 89353, 40, 131, '2025-06-01 21:16:21.000000', '2024-05-04 22:54:47.000000', 1, 'ACTIVE'), -(3, 53146, 45, 132, '2024-10-26 17:40:25.000000', '2024-09-03 16:05:27.000000', 2, 'CANCELLED'), -(2, 89466, 27, 133, '2024-01-13 21:17:12.000000', '2025-03-30 12:28:29.000000', 1, 'ACTIVE'), -(1, 45623, 20, 134, '2025-04-21 07:29:07.000000', '2024-01-21 16:21:17.000000', 2, 'CANCELLED'), -(3, 56394, 39, 135, '2023-09-25 18:06:16.000000', '2024-10-03 02:48:34.000000', 1, 'ACTIVE'), -(3, 62929, 10, 136, '2025-07-28 06:38:54.000000', '2023-08-14 19:09:56.000000', 2, 'CANCELLED'), -(1, 95374, 23, 137, '2023-09-11 20:58:20.000000', '2024-01-28 17:42:38.000000', 2, 'ACTIVE'), -(1, 88339, 50, 138, '2024-03-14 21:04:19.000000', '2024-10-07 14:20:30.000000', 1, 'CANCELLED'), -(1, 61843, 31, 139, '2024-07-27 12:40:25.000000', '2023-12-10 00:21:31.000000', 1, 'ACTIVE'), -(2, 77859, 23, 140, '2023-11-18 17:34:25.000000', '2025-05-24 05:07:22.000000', 1, 'CANCELLED'), -(2, 79137, 26, 141, '2025-07-02 23:51:04.000000', '2024-04-03 20:24:42.000000', 1, 'ACTIVE'), -(2, 86877, 24, 142, '2023-11-17 23:29:21.000000', '2024-09-07 14:30:38.000000', 1, 'CANCELLED'), -(3, 75421, 46, 143, '2023-10-20 22:58:28.000000', '2024-01-17 07:34:27.000000', 2, 'ACTIVE'), -(2, 51857, 24, 144, '2024-10-19 01:48:40.000000', '2024-04-10 22:19:39.000000', 1, 'CANCELLED'), -(1, 30226, 47, 145, '2024-01-15 02:00:12.000000', '2023-10-16 14:19:12.000000', 1, 'ACTIVE'), -(2, 69521, 48, 146, '2023-10-15 18:25:11.000000', '2024-12-04 07:19:33.000000', 2, 'CANCELLED'), -(1, 99201, 33, 147, '2024-12-07 04:59:14.000000', '2025-06-28 23:45:54.000000', 2, 'ACTIVE'), -(2, 33024, 3, 148, '2023-10-29 06:25:59.000000', '2023-10-01 16:34:53.000000', 2, 'CANCELLED'), -(3, 12964, 17, 149, '2024-04-10 10:57:41.000000', '2025-05-20 15:32:08.000000', 2, 'ACTIVE'), -(2, 78802, 40, 150, '2024-09-24 04:49:33.000000', '2023-09-04 04:21:39.000000', 1, 'CANCELLED'), -(1, 64340, 9, 151, '2024-03-27 04:20:37.000000', '2025-05-10 03:09:48.000000', 1, 'ACTIVE'), -(2, 74913, 20, 152, '2023-08-30 09:34:05.000000', '2024-09-28 14:02:58.000000', 1, 'CANCELLED'), -(3, 9495, 12, 153, '2023-12-29 04:11:47.000000', '2024-01-30 03:48:28.000000', 2, 'ACTIVE'), -(1, 69288, 12, 154, '2024-12-01 10:19:41.000000', '2024-02-17 23:27:06.000000', 2, 'CANCELLED'), -(2, 72433, 13, 155, '2024-09-18 14:00:07.000000', '2025-07-18 15:10:53.000000', 2, 'ACTIVE'), -(3, 8666, 36, 156, '2024-10-07 21:40:20.000000', '2024-04-17 12:29:04.000000', 2, 'CANCELLED'), -(2, 18204, 18, 157, '2023-10-11 03:16:38.000000', '2025-03-13 18:42:56.000000', 2, 'ACTIVE'), -(3, 1886, 38, 158, '2023-08-07 13:39:40.000000', '2024-10-27 19:17:08.000000', 2, 'CANCELLED'), -(3, 15685, 2, 159, '2025-06-13 05:19:48.000000', '2024-07-13 12:17:48.000000', 1, 'ACTIVE'), -(3, 13131, 35, 160, '2024-03-30 04:11:31.000000', '2025-07-05 15:18:28.000000', 2, 'CANCELLED'), -(2, 41916, 13, 161, '2023-09-13 21:37:58.000000', '2024-12-09 17:32:42.000000', 2, 'ACTIVE'), -(1, 49512, 36, 162, '2024-01-31 01:09:39.000000', '2024-04-06 13:12:31.000000', 1, 'CANCELLED'), -(1, 52705, 9, 163, '2024-10-18 04:32:23.000000', '2025-04-04 21:39:52.000000', 2, 'ACTIVE'), -(3, 64560, 19, 164, '2023-09-03 01:54:37.000000', '2025-01-11 12:03:52.000000', 2, 'CANCELLED'), -(1, 52744, 23, 165, '2023-10-30 12:46:34.000000', '2025-06-28 16:45:04.000000', 2, 'ACTIVE'), -(1, 64767, 36, 166, '2025-07-22 21:53:47.000000', '2024-11-24 23:33:35.000000', 2, 'CANCELLED'), -(3, 28128, 15, 167, '2024-02-14 12:38:57.000000', '2025-07-22 03:28:24.000000', 2, 'ACTIVE'), -(2, 19125, 1, 168, '2023-10-19 21:30:04.000000', '2024-12-29 01:42:57.000000', 1, 'CANCELLED'), -(2, 66523, 32, 169, '2024-09-23 20:32:01.000000', '2025-07-10 08:58:18.000000', 1, 'ACTIVE'), -(3, 23536, 7, 170, '2024-02-06 23:16:20.000000', '2023-10-20 13:35:10.000000', 1, 'CANCELLED'), -(2, 61813, 47, 171, '2023-09-23 08:05:07.000000', '2024-12-23 13:36:16.000000', 1, 'ACTIVE'), -(2, 29546, 47, 172, '2024-04-17 01:30:07.000000', '2025-03-19 09:09:17.000000', 1, 'CANCELLED'), -(3, 94167, 13, 173, '2024-01-30 13:41:53.000000', '2024-06-07 06:00:23.000000', 2, 'ACTIVE'), -(2, 8775, 37, 174, '2023-12-30 22:12:29.000000', '2023-11-23 16:21:33.000000', 1, 'CANCELLED'), -(1, 27083, 18, 175, '2023-09-09 12:10:05.000000', '2023-11-15 22:14:01.000000', 1, 'ACTIVE'), -(1, 41882, 45, 176, '2024-04-17 16:17:06.000000', '2024-07-28 05:16:11.000000', 2, 'CANCELLED'), -(2, 99376, 41, 177, '2024-06-28 12:19:27.000000', '2023-08-28 09:24:35.000000', 2, 'ACTIVE'), -(2, 66588, 13, 178, '2024-05-27 16:12:17.000000', '2024-06-02 18:20:53.000000', 1, 'CANCELLED'), -(1, 79295, 10, 179, '2023-12-26 02:33:02.000000', '2023-11-19 10:36:14.000000', 2, 'ACTIVE'), -(2, 73053, 44, 180, '2025-07-07 09:43:48.000000', '2025-01-27 20:01:03.000000', 1, 'CANCELLED'), -(2, 10475, 40, 181, '2024-05-22 14:52:04.000000', '2023-09-28 01:24:40.000000', 2, 'ACTIVE'), -(1, 53739, 48, 182, '2023-11-29 04:17:21.000000', '2024-02-21 06:11:26.000000', 1, 'CANCELLED'), -(2, 63675, 25, 183, '2023-11-26 15:33:01.000000', '2023-08-28 19:31:44.000000', 2, 'ACTIVE'), -(1, 65450, 13, 184, '2024-06-19 13:28:48.000000', '2024-09-27 05:59:51.000000', 1, 'CANCELLED'), -(1, 65977, 7, 185, '2024-10-20 01:11:55.000000', '2024-07-17 21:37:15.000000', 2, 'ACTIVE'), -(1, 93381, 20, 186, '2025-01-08 18:34:34.000000', '2024-03-13 00:41:10.000000', 2, 'CANCELLED'), -(1, 19546, 23, 187, '2024-01-26 08:59:34.000000', '2025-05-06 05:50:18.000000', 1, 'ACTIVE'), -(3, 19267, 32, 188, '2023-08-14 18:52:18.000000', '2024-03-21 06:32:54.000000', 2, 'CANCELLED'), -(2, 1497, 8, 189, '2025-01-10 01:37:35.000000', '2024-05-14 07:25:32.000000', 2, 'ACTIVE'), -(2, 16104, 35, 190, '2024-07-15 05:23:09.000000', '2023-10-24 04:33:21.000000', 1, 'CANCELLED'), -(3, 17568, 14, 191, '2024-10-11 02:39:10.000000', '2023-09-05 20:35:06.000000', 2, 'ACTIVE'), -(2, 79352, 19, 192, '2025-02-15 19:17:37.000000', '2024-04-15 19:14:41.000000', 2, 'CANCELLED'), -(1, 89050, 23, 193, '2024-01-28 21:59:16.000000', '2025-05-26 14:03:51.000000', 2, 'ACTIVE'), -(2, 27288, 34, 194, '2023-12-27 00:48:47.000000', '2024-01-04 05:38:00.000000', 1, 'CANCELLED'), -(3, 53570, 34, 195, '2024-12-05 01:03:02.000000', '2024-03-05 00:43:52.000000', 1, 'ACTIVE'), -(2, 91222, 31, 196, '2024-07-14 20:46:31.000000', '2025-03-15 13:13:35.000000', 1, 'CANCELLED'), -(1, 55143, 25, 197, '2023-08-11 05:31:08.000000', '2024-09-12 12:07:55.000000', 2, 'ACTIVE'), -(3, 89508, 17, 198, '2024-03-18 11:52:51.000000', '2025-04-01 02:29:30.000000', 1, 'CANCELLED'), -(3, 42560, 7, 199, '2024-02-01 21:04:06.000000', '2024-03-02 18:23:16.000000', 1, 'ACTIVE'), -(2, 80080, 20, 200, '2024-07-31 01:24:39.000000', '2024-05-02 17:01:02.000000', 2, 'CANCELLED'); -/*!40000 ALTER TABLE `bid` ENABLE KEYS */; -UNLOCK TABLES; - -LOCK TABLES `image` WRITE; -/*!40000 ALTER TABLE `image` DISABLE KEYS */; -INSERT INTO `image` VALUES ('2023-12-12 00:50:36.000000',1,37,'2024-08-04 16:17:45.000000','?ab=441&aad=2'),('2024-08-08 04:45:22.000000',2,44,'2024-02-17 13:49:41.000000','?q=test'),('2025-06-09 12:19:24.000000',3,31,'2025-03-08 17:28:12.000000','?str=se'),('2024-03-03 03:22:10.000000',4,30,'2023-10-06 20:57:54.000000','?gi=100'),('2024-09-21 03:17:21.000000',5,2,'2023-10-22 18:50:07.000000','?q=0'),('2024-07-01 17:00:25.000000',6,7,'2025-04-19 16:15:20.000000','?gi=100'),('2024-03-05 17:31:15.000000',7,39,'2024-09-23 19:07:10.000000','?q=0'),('2024-12-31 20:44:29.000000',8,22,'2025-05-10 01:48:52.000000','?client=g'),('2025-06-22 12:39:33.000000',9,29,'2024-08-06 06:19:32.000000','?page=1&offset=1'),('2025-07-18 16:09:47.000000',10,44,'2025-03-23 08:51:23.000000','?q=0'),('2025-04-27 19:55:04.000000',11,12,'2024-06-03 03:21:09.000000','?ad=115'),('2025-01-09 08:49:12.000000',12,16,'2024-07-20 15:52:30.000000','?q=4'),('2024-11-25 18:03:20.000000',13,4,'2025-03-30 05:10:37.000000','?str=se'),('2023-12-18 07:50:10.000000',14,15,'2024-03-30 06:31:33.000000','?q=test'),('2024-04-28 03:44:12.000000',15,42,'2024-05-22 18:35:13.000000','?q=test'),('2024-03-14 02:38:10.000000',16,45,'2024-03-16 17:35:29.000000','?q=4'),('2025-05-12 12:10:24.000000',17,36,'2024-07-02 13:06:11.000000','?k=0'),('2023-08-22 16:30:19.000000',18,38,'2025-01-22 01:27:30.000000','?g=1'),('2024-08-22 01:51:40.000000',19,45,'2024-04-07 22:06:13.000000','?q=test'),('2024-06-14 03:45:34.000000',20,27,'2023-12-25 23:27:03.000000','?k=0'),('2023-11-04 06:00:47.000000',21,31,'2025-04-12 10:59:43.000000','?q=0'),('2024-02-14 00:13:25.000000',22,43,'2025-04-05 14:31:33.000000','?ab=441&aad=2'),('2023-09-25 03:16:23.000000',23,20,'2025-01-02 09:57:51.000000','?search=1'),('2025-02-15 13:17:22.000000',24,41,'2025-03-18 00:45:43.000000','?k=0'),('2024-12-19 12:12:20.000000',25,11,'2024-09-21 19:18:27.000000','?q=11'),('2024-09-12 16:30:44.000000',26,23,'2024-11-21 20:42:23.000000','?q=11'),('2024-01-18 05:56:40.000000',27,34,'2025-03-31 17:56:33.000000','?q=11'),('2024-06-03 07:56:06.000000',28,39,'2024-11-17 16:35:44.000000','?gi=100'),('2024-01-24 06:27:31.000000',29,45,'2023-08-28 08:26:11.000000','?page=1&offset=1'),('2025-07-15 04:12:59.000000',30,2,'2025-07-31 11:21:15.000000','?gi=100'),('2025-06-25 17:25:15.000000',31,13,'2024-09-21 23:57:07.000000','?q=0'),('2025-07-23 22:13:07.000000',32,33,'2025-08-01 03:26:55.000000','?k=0'),('2023-08-19 17:24:58.000000',33,29,'2024-11-29 06:48:38.000000','?p=8'),('2024-01-25 17:51:40.000000',34,31,'2024-09-13 04:46:53.000000','?str=se'),('2024-11-11 12:20:23.000000',35,20,'2024-10-03 13:37:42.000000','?q=11'),('2024-08-18 16:39:17.000000',36,5,'2025-05-24 22:15:28.000000','?q=4'),('2024-01-28 04:26:27.000000',37,3,'2024-07-23 02:33:50.000000','?search=1'),('2024-07-01 22:48:10.000000',38,7,'2025-01-30 08:58:35.000000','?q=4'),('2024-01-05 17:03:13.000000',39,20,'2024-08-31 13:58:36.000000','?q=0'),('2024-08-26 11:17:20.000000',40,47,'2024-06-15 07:12:23.000000','?g=1'),('2024-01-24 19:52:44.000000',41,22,'2024-05-16 08:05:54.000000','?ab=441&aad=2'),('2024-02-14 06:59:54.000000',42,15,'2025-07-19 13:32:24.000000','?g=1'),('2024-03-29 07:19:42.000000',43,35,'2024-05-26 03:32:45.000000','?page=1&offset=1'),('2024-03-22 20:10:49.000000',44,50,'2024-03-24 13:46:28.000000','?str=se'),('2024-03-14 02:31:52.000000',45,7,'2023-09-03 07:58:45.000000','?search=1&q=de'),('2023-11-19 23:49:40.000000',46,37,'2024-04-18 16:09:08.000000','?k=0'),('2024-07-16 05:43:50.000000',47,42,'2024-03-24 05:26:24.000000','?q=4'),('2025-04-02 16:23:46.000000',48,15,'2023-10-26 15:32:20.000000','?g=1'),('2024-09-03 21:03:50.000000',49,23,'2024-03-15 14:03:18.000000','?q=4'),('2025-05-13 03:12:56.000000',50,35,'2024-06-24 10:30:29.000000','?str=se'),('2024-10-20 13:07:38.000000',51,44,'2024-09-01 21:33:28.000000','?g=1'),('2023-11-05 12:19:53.000000',52,6,'2023-08-14 23:53:57.000000','?page=1&offset=1'),('2025-07-31 22:57:55.000000',53,28,'2025-06-20 18:52:33.000000','?p=8'),('2025-03-25 11:34:40.000000',54,47,'2023-12-05 22:32:46.000000','?client=g'),('2024-12-09 06:28:36.000000',55,35,'2024-08-23 19:09:27.000000','?search=1&q=de'),('2025-06-16 09:05:08.000000',56,27,'2024-03-16 20:22:36.000000','?q=4'),('2025-07-14 19:38:04.000000',57,47,'2025-04-01 11:32:23.000000','?k=0'),('2025-07-09 17:02:44.000000',58,5,'2023-11-16 21:24:00.000000','?q=11'),('2024-06-07 16:48:40.000000',59,25,'2024-03-05 07:05:21.000000','?q=11'),('2025-01-22 14:00:45.000000',60,30,'2024-01-07 23:01:11.000000','?search=1'),('2025-06-24 05:16:04.000000',61,15,'2025-07-05 11:05:32.000000','?q=4'),('2023-12-21 20:20:59.000000',62,44,'2025-01-19 18:59:13.000000','?gi=100'),('2024-08-17 21:53:36.000000',63,31,'2023-11-24 08:12:03.000000','?search=1&q=de'),('2025-02-15 07:22:22.000000',64,32,'2024-05-21 01:14:39.000000','?search=1&q=de'),('2024-08-20 20:44:57.000000',65,49,'2024-12-12 22:16:13.000000','?q=test'),('2025-04-06 00:21:40.000000',66,33,'2024-08-09 00:14:06.000000','?q=test'),('2024-09-20 15:18:04.000000',67,36,'2025-02-27 23:13:56.000000','?page=1&offset=1'),('2024-03-01 02:21:17.000000',68,48,'2024-07-01 21:52:40.000000','?page=1&offset=1'),('2023-08-25 20:13:41.000000',69,17,'2025-06-20 07:50:56.000000','?client=g'),('2024-05-06 20:22:19.000000',70,32,'2025-04-15 02:59:36.000000','?g=1'),('2025-07-05 12:14:34.000000',71,1,'2024-06-24 14:16:10.000000','?search=1'),('2025-02-21 13:10:48.000000',72,24,'2025-07-28 11:51:36.000000','?ad=115'),('2023-10-22 19:11:54.000000',73,5,'2024-02-25 03:26:18.000000','?str=se'),('2024-02-12 22:10:03.000000',74,49,'2024-10-01 09:31:59.000000','?q=0'),('2024-05-23 16:00:12.000000',75,28,'2024-03-29 00:02:44.000000','?g=1'),('2024-01-01 07:22:52.000000',76,15,'2024-12-29 22:40:44.000000','?gi=100'),('2024-05-03 09:48:36.000000',77,39,'2024-03-16 11:04:30.000000','?q=4'),('2024-03-16 06:54:01.000000',78,48,'2023-09-30 11:04:12.000000','?ab=441&aad=2'),('2025-01-24 16:40:16.000000',79,23,'2024-02-12 01:53:27.000000','?g=1'),('2023-10-22 19:08:58.000000',80,50,'2025-03-10 04:00:14.000000','?search=1'),('2025-07-19 17:06:38.000000',81,22,'2025-03-09 19:42:36.000000','?p=8'),('2025-06-25 08:56:23.000000',82,20,'2023-12-17 11:10:36.000000','?search=1&q=de'),('2024-08-10 11:16:39.000000',83,17,'2025-03-31 21:36:33.000000','?gi=100'),('2025-04-02 09:55:28.000000',84,23,'2024-05-18 11:30:27.000000','?ad=115'),('2025-07-30 15:40:06.000000',85,12,'2024-02-27 04:41:21.000000','?q=11'),('2024-06-15 19:09:57.000000',86,20,'2024-07-08 17:19:20.000000','?q=0'),('2025-02-07 14:37:30.000000',87,26,'2025-07-12 09:08:53.000000','?q=0'),('2025-07-14 01:45:21.000000',88,35,'2023-10-30 18:46:01.000000','?ab=441&aad=2'),('2024-08-05 04:17:44.000000',89,7,'2024-03-07 18:54:34.000000','?gi=100'),('2024-01-26 17:15:45.000000',90,3,'2023-12-16 22:58:49.000000','?p=8'),('2024-02-08 15:48:44.000000',91,18,'2024-06-10 11:20:44.000000','?ad=115'),('2024-02-28 13:12:35.000000',92,3,'2023-12-08 12:33:16.000000','?q=11'),('2025-04-01 16:13:15.000000',93,28,'2025-02-06 05:18:20.000000','?g=1'),('2023-09-16 06:09:36.000000',94,6,'2024-05-21 06:23:51.000000','?q=11'),('2024-11-19 19:41:47.000000',95,7,'2024-12-12 23:17:19.000000','?q=test'),('2025-05-25 00:56:58.000000',96,19,'2024-05-26 21:37:07.000000','?search=1&q=de'),('2023-11-14 19:31:39.000000',97,35,'2023-12-17 21:56:19.000000','?page=1&offset=1'),('2025-04-26 02:39:29.000000',98,2,'2024-04-17 09:19:16.000000','?search=1&q=de'),('2024-12-20 03:35:04.000000',99,26,'2023-12-18 11:40:08.000000','?p=8'),('2024-01-10 07:09:26.000000',100,24,'2024-09-27 13:17:00.000000','?p=8'); -/*!40000 ALTER TABLE `image` ENABLE KEYS */; -UNLOCK TABLES; - - -LOCK TABLES `like_table` WRITE; -/*!40000 ALTER TABLE `like_table` DISABLE KEYS */; -INSERT INTO `like_table` VALUES ('2024-01-01 12:00:00.000000',1,1,'2024-01-01 12:05:00.000000',1),('2024-01-02 13:00:00.000000',2,2,'2024-01-02 13:05:00.000000',1),('2024-01-03 14:00:00.000000',3,3,'2024-01-03 14:05:00.000000',1),('2024-01-04 15:00:00.000000',4,4,'2024-01-04 15:05:00.000000',1),('2024-01-05 16:00:00.000000',5,5,'2024-01-05 16:05:00.000000',1),('2024-01-06 17:00:00.000000',6,6,'2024-01-06 17:05:00.000000',1),('2024-01-07 18:00:00.000000',7,7,'2024-01-07 18:05:00.000000',1),('2024-01-08 19:00:00.000000',8,8,'2024-01-08 19:05:00.000000',1),('2024-01-09 20:00:00.000000',9,9,'2024-01-09 20:05:00.000000',1),('2024-01-10 21:00:00.000000',10,10,'2024-01-10 21:05:00.000000',1),('2024-01-11 22:00:00.000000',11,11,'2024-01-11 22:05:00.000000',1),('2024-01-12 23:00:00.000000',12,12,'2024-01-12 23:05:00.000000',1),('2024-01-13 00:00:00.000000',13,13,'2024-01-13 00:05:00.000000',1),('2024-01-14 01:00:00.000000',14,14,'2024-01-14 01:05:00.000000',1),('2024-01-15 02:00:00.000000',15,15,'2024-01-15 02:05:00.000000',1),('2024-01-16 03:00:00.000000',16,16,'2024-01-16 03:05:00.000000',1),('2024-01-17 04:00:00.000000',17,17,'2024-01-17 04:05:00.000000',1),('2024-01-18 05:00:00.000000',18,18,'2024-01-18 05:05:00.000000',1),('2024-01-19 06:00:00.000000',19,19,'2024-01-19 06:05:00.000000',1),('2024-01-20 07:00:00.000000',20,20,'2024-01-20 07:05:00.000000',1),('2024-01-21 08:00:00.000000',21,21,'2024-01-21 08:05:00.000000',1),('2024-01-22 09:00:00.000000',22,22,'2024-01-22 09:05:00.000000',1),('2024-01-23 10:00:00.000000',23,23,'2024-01-23 10:05:00.000000',1),('2024-01-24 11:00:00.000000',24,24,'2024-01-24 11:05:00.000000',1),('2024-01-25 12:00:00.000000',25,25,'2024-01-25 12:05:00.000000',1),('2024-01-26 13:00:00.000000',26,26,'2024-01-26 13:05:00.000000',1),('2024-01-27 14:00:00.000000',27,27,'2024-01-27 14:05:00.000000',1),('2024-01-28 15:00:00.000000',28,28,'2024-01-28 15:05:00.000000',1),('2024-01-29 16:00:00.000000',29,29,'2024-01-29 16:05:00.000000',1),('2024-01-30 17:00:00.000000',30,30,'2024-01-30 17:05:00.000000',1),('2024-02-01 12:00:00.000000',31,1,'2024-02-01 12:05:00.000000',2),('2024-02-02 13:00:00.000000',32,2,'2024-02-02 13:05:00.000000',2),('2024-02-03 14:00:00.000000',33,3,'2024-02-03 14:05:00.000000',2),('2024-02-04 15:00:00.000000',34,4,'2024-02-04 15:05:00.000000',2),('2024-02-05 16:00:00.000000',35,5,'2024-02-05 16:05:00.000000',2),('2024-02-06 17:00:00.000000',36,6,'2024-02-06 17:05:00.000000',2),('2024-02-07 18:00:00.000000',37,7,'2024-02-07 18:05:00.000000',2),('2024-02-08 19:00:00.000000',38,8,'2024-02-08 19:05:00.000000',2),('2024-02-09 20:00:00.000000',39,9,'2024-02-09 20:05:00.000000',2),('2024-02-10 21:00:00.000000',40,10,'2024-02-10 21:05:00.000000',2),('2024-02-11 22:00:00.000000',41,11,'2024-02-11 22:05:00.000000',2),('2024-02-12 23:00:00.000000',42,12,'2024-02-12 23:05:00.000000',2),('2024-02-13 00:00:00.000000',43,13,'2024-02-13 00:05:00.000000',2),('2024-02-14 01:00:00.000000',44,14,'2024-02-14 01:05:00.000000',2),('2024-02-15 02:00:00.000000',45,15,'2024-02-15 02:05:00.000000',2),('2024-02-16 03:00:00.000000',46,16,'2024-02-16 03:05:00.000000',2),('2024-02-17 04:00:00.000000',47,17,'2024-02-17 04:05:00.000000',2),('2024-02-18 05:00:00.000000',48,18,'2024-02-18 05:05:00.000000',2),('2024-02-19 06:00:00.000000',49,19,'2024-02-19 06:05:00.000000',2),('2024-02-20 07:00:00.000000',50,20,'2024-02-20 07:05:00.000000',2),('2024-02-21 08:00:00.000000',51,21,'2024-02-21 08:05:00.000000',2),('2024-02-22 09:00:00.000000',52,22,'2024-02-22 09:05:00.000000',2),('2024-02-23 10:00:00.000000',53,23,'2024-02-23 10:05:00.000000',2),('2024-02-24 11:00:00.000000',54,24,'2024-02-24 11:05:00.000000',2),('2024-02-25 12:00:00.000000',55,25,'2024-02-25 12:05:00.000000',2),('2024-02-26 13:00:00.000000',56,26,'2024-02-26 13:05:00.000000',2),('2024-02-27 14:00:00.000000',57,27,'2024-02-27 14:05:00.000000',2),('2024-02-28 15:00:00.000000',58,28,'2024-02-28 15:05:00.000000',2),('2024-02-29 16:00:00.000000',59,29,'2024-02-29 16:05:00.000000',2),('2024-03-01 17:00:00.000000',60,30,'2024-03-01 17:05:00.000000',2); -/*!40000 ALTER TABLE `like_table` ENABLE KEYS */; -UNLOCK TABLES; - - -LOCK TABLES `payment` WRITE; -/*!40000 ALTER TABLE `payment` DISABLE KEYS */; -INSERT INTO `payment` VALUES (1,18,'2024-09-12 04:34:52.000000',1,'2025-06-27 13:40:17.000000',1,'43A27856-7DBC-3E9B-EA77-EB6B834D879A','UKM15OHW5KK','READY','CARD'),(3,8,'2024-12-01 10:32:00.000000',2,'2023-09-24 20:02:11.000000',2,'77FDCC5D-9676-7F2B-4678-93B247495AD5','HEB08JLB3JC','IN_PROGRESS','EASY_PAYMENT'),(4,33,'2025-07-26 15:50:09.000000',3,'2024-05-30 12:43:20.000000',2,'9E86ED61-994B-8B76-41AD-48846C71C52B','MVK13ATM8YI','WAITING_FOR_DEPOSIT','MOBILE'),(5,28,'2025-05-03 04:05:48.000000',4,'2023-11-14 05:24:52.000000',2,'2F2DC526-A187-14BE-F6E1-63A0E2459249','VMT29NNQ3HA','DONE','ACCOUNT_TRANSFER'),(4,44,'2025-07-30 14:54:23.000000',5,'2025-06-23 00:03:38.000000',2,'3C7A41CD-3970-837E-7208-83599BB58423','WQM39SXE5AF','CANCELED','VIRTUAL_ACCOUNT'),(2,16,'2025-02-08 14:21:37.000000',6,'2024-04-21 03:51:12.000000',2,'14CAB2E4-B93C-8FE2-9759-15892ED3CBE8','WRM53OWV5LQ','PARTIAL_CANCELED','CULTURE_GIFT_CARD'),(5,4,'2024-10-25 07:36:01.000000',7,'2024-04-12 23:46:33.000000',1,'56BC67A4-224E-805A-D87C-242662DB8B00','KQE78EHR2DG','ABORTED','BOOK_CULTURE_GIFT_CARD'),(2,33,'2025-06-04 09:26:33.000000',8,'2025-03-12 19:21:09.000000',1,'1EEC266C-957B-6688-6D52-B511C0DA2C1A','VVI18YYS6PC','EXPIRED','GAME_CULTURE_GIFT_CARD'),(3,32,'2024-07-28 07:07:02.000000',9,'2025-04-07 19:03:52.000000',1,'2A33BFBE-D192-5D7E-8E9F-7CEAA7FB297E','AWW90GBW3CX','READY','CASH'),(2,9,'2023-12-12 06:32:10.000000',10,'2024-02-09 17:57:06.000000',2,'B36E5B95-D539-05D3-DCB4-B64D80749548','GUH16EVO3PT','IN_PROGRESS','CARD'),(4,44,'2024-11-16 05:57:03.000000',11,'2024-10-12 10:59:58.000000',1,'387925BB-BD81-3744-4D53-64DC640AA484','LOH23RVG8IQ','WAITING_FOR_DEPOSIT','EASY_PAYMENT'),(3,12,'2025-05-19 19:52:59.000000',12,'2024-11-20 09:31:00.000000',2,'E91E017C-74BE-5E88-9755-5C85396C647D','TEF62FOO4JL','DONE','MOBILE'),(4,3,'2024-02-04 18:51:48.000000',13,'2023-12-18 01:04:10.000000',2,'B7E49743-6331-E432-4A30-77803A18EF5C','YBW57UEC4WI','CANCELED','ACCOUNT_TRANSFER'),(3,9,'2024-07-07 20:42:31.000000',14,'2023-11-16 01:09:29.000000',2,'902C1B92-35DA-93C5-7588-35C966278827','JIQ35WXG6EW','PARTIAL_CANCELED','VIRTUAL_ACCOUNT'),(3,20,'2023-10-29 19:41:42.000000',15,'2024-03-22 03:21:03.000000',2,'298A5526-6AA0-3C38-FF29-A46271FF85EB','NOK25WVY4OY','ABORTED','CULTURE_GIFT_CARD'),(3,15,'2024-08-16 00:59:03.000000',16,'2024-07-28 06:31:26.000000',1,'AA9FFE26-1EA7-5CC4-D973-AC44EA592D4D','VBQ35KOE4WY','EXPIRED','BOOK_CULTURE_GIFT_CARD'),(4,18,'2024-07-22 18:50:10.000000',17,'2025-01-30 04:43:43.000000',2,'C110842D-C142-D4EF-189A-806AB397D744','JFX98GFW1HL','READY','GAME_CULTURE_GIFT_CARD'),(2,10,'2025-01-11 21:21:32.000000',18,'2024-08-28 13:46:27.000000',2,'F811D18B-6776-93CB-3EB5-560BC8692CAB','GVS55VIA9GG','IN_PROGRESS','CASH'),(2,48,'2025-07-04 21:25:34.000000',19,'2025-01-03 05:33:26.000000',2,'B9DA1476-E921-AA3A-0505-D0387A45AAA1','SBP74LOG6HG','WAITING_FOR_DEPOSIT','CARD'),(2,48,'2023-12-23 03:28:59.000000',20,'2024-10-24 19:45:40.000000',2,'A415FE33-BA22-BBD9-A76B-82D18B454282','ZVX20EKP8PP','DONE','EASY_PAYMENT'),(4,29,'2025-07-29 10:10:46.000000',21,'2024-11-28 01:48:41.000000',2,'09E11AED-84C5-AF71-95E9-DB6C24F189A1','XQE34KTI1KX','CANCELED','MOBILE'),(3,30,'2025-05-08 21:53:16.000000',22,'2024-11-22 14:59:38.000000',2,'BF4C2D99-B305-AB46-E747-A9832CEA8D34','XSD90XJQ5CO','PARTIAL_CANCELED','ACCOUNT_TRANSFER'),(4,3,'2024-04-27 03:52:01.000000',23,'2025-05-14 19:38:40.000000',2,'37C7C4E9-9153-AF5E-EA89-5A3821624FA6','CJN60VBG9UL','ABORTED','VIRTUAL_ACCOUNT'),(3,8,'2024-12-24 07:16:09.000000',24,'2025-04-15 08:08:35.000000',2,'60BD0236-0C85-D48F-901C-E880A341D3C6','HTD42WQE9GJ','EXPIRED','CULTURE_GIFT_CARD'),(2,35,'2023-09-16 16:43:35.000000',25,'2024-04-01 06:12:49.000000',1,'EB346CE3-729A-C8A8-38D4-F33979526842','GNI56RGR8OH','READY','BOOK_CULTURE_GIFT_CARD'),(5,26,'2024-12-21 14:46:19.000000',26,'2024-07-29 09:40:13.000000',2,'F9AA47C3-9C5F-AD8E-2923-A4671ECD4568','SMR38RGF8FF','IN_PROGRESS','GAME_CULTURE_GIFT_CARD'),(1,41,'2025-02-12 10:57:21.000000',27,'2024-08-20 01:06:37.000000',1,'27A20916-928A-E2A6-D9DF-494A5DF92FEC','FAD52LDB6PD','WAITING_FOR_DEPOSIT','CASH'),(4,38,'2024-04-15 21:54:52.000000',28,'2023-11-26 00:57:51.000000',1,'6B98AF37-DD91-D41A-2740-E656E205198A','CYR16FPX4NG','DONE','CARD'),(3,26,'2025-01-24 11:01:20.000000',29,'2024-12-31 16:28:50.000000',2,'31101C6A-B3FE-DA75-1BD2-9CE54962D981','GQJ88MPO3EB','CANCELED','EASY_PAYMENT'),(4,17,'2025-01-17 20:20:41.000000',30,'2025-04-16 21:08:26.000000',1,'0F84FF5E-B9EC-4A33-FCAF-4593DCBAABE6','BRX41RUJ4BL','PARTIAL_CANCELED','MOBILE'),(1,15,'2024-02-03 21:58:21.000000',31,'2025-07-16 02:40:23.000000',1,'65ECDA0F-8C77-B133-FEC9-B91BEE6D98C2','SVC28GQX6GF','ABORTED','ACCOUNT_TRANSFER'),(2,44,'2024-02-08 14:20:21.000000',32,'2024-10-24 00:04:59.000000',1,'30AED4BB-3BCD-6E25-66E6-5A9620779F8A','HCS38TTD8CE','EXPIRED','VIRTUAL_ACCOUNT'),(2,22,'2023-12-24 06:16:23.000000',33,'2023-12-10 06:50:17.000000',2,'A64D8324-2885-AE28-8CEA-A26D871C6975','SMH71IEG4EX','READY','CULTURE_GIFT_CARD'),(3,45,'2025-01-28 03:41:06.000000',34,'2024-01-23 17:07:01.000000',1,'589D0363-9971-E511-9852-E64A8A8DC8D0','PLW21RYK7OU','IN_PROGRESS','BOOK_CULTURE_GIFT_CARD'),(4,36,'2023-09-19 07:29:24.000000',35,'2023-11-02 22:38:50.000000',1,'BE759324-91D9-657E-DB52-6127508DA4C5','XYJ26INX9AB','WAITING_FOR_DEPOSIT','GAME_CULTURE_GIFT_CARD'),(5,41,'2025-07-30 22:48:04.000000',36,'2024-06-06 22:20:45.000000',2,'208D1CC7-1A47-83F5-8929-FAB6C1AD2A93','VAX74LGK2US','DONE','CASH'),(3,25,'2025-06-14 10:10:14.000000',37,'2024-10-30 17:38:32.000000',1,'784C5276-4D62-ACC4-9970-6F607476C9C6','ULZ93TXN7LL','CANCELED','CARD'),(3,25,'2024-04-05 11:54:58.000000',38,'2025-05-17 08:07:33.000000',2,'2430E674-5E6F-3A48-03C6-53D0F94D41A5','KHX82BRQ3FP','PARTIAL_CANCELED','EASY_PAYMENT'),(3,17,'2024-09-25 16:37:28.000000',39,'2025-06-10 13:09:50.000000',1,'5A369DBE-D094-51AA-98CD-166BE42E8144','QRH14VPN1GU','ABORTED','MOBILE'),(2,41,'2025-02-23 22:56:37.000000',40,'2023-10-24 12:50:03.000000',2,'42EB3BE1-48D2-9D74-2E97-5B95EDD9B58B','UMU53SER1NV','EXPIRED','ACCOUNT_TRANSFER'),(3,23,'2024-08-18 03:19:01.000000',41,'2025-01-18 01:09:14.000000',2,'D831BE46-58BD-D13D-D698-AB54953637C2','IEP09VTX1IP','READY','VIRTUAL_ACCOUNT'),(1,32,'2025-01-22 06:30:53.000000',42,'2023-12-13 01:37:33.000000',2,'88A56155-A4B5-CC26-18E9-C2AD2A48899F','AGY48LLK6DB','IN_PROGRESS','CULTURE_GIFT_CARD'),(2,11,'2024-04-11 17:44:37.000000',43,'2025-05-09 10:56:09.000000',1,'E5AAAE19-9001-951D-6C8C-977B44D34FED','TCY94PLO2DN','WAITING_FOR_DEPOSIT','BOOK_CULTURE_GIFT_CARD'),(3,24,'2024-08-20 18:14:43.000000',44,'2023-11-10 16:42:50.000000',2,'3ED8DC1E-3791-9DFB-2879-3BABE5B47DDC','RLK70SLI9UW','DONE','GAME_CULTURE_GIFT_CARD'),(4,43,'2024-01-13 20:53:17.000000',45,'2025-02-25 03:43:33.000000',2,'E19DF27E-85F9-B2B9-6970-D1C8E6CE2D48','KFS46NMM1DV','CANCELED','CASH'),(5,34,'2023-11-28 21:12:39.000000',46,'2025-06-18 14:35:19.000000',1,'07CF6DBB-666E-C5A2-52B2-984ACFE6C514','HNW11KLJ4DX','PARTIAL_CANCELED','CARD'),(3,44,'2023-10-24 12:16:59.000000',47,'2024-07-24 05:43:52.000000',1,'F16C4132-2E3D-2C61-41F8-533E4FD95290','EEC65EPM8PH','ABORTED','EASY_PAYMENT'),(5,27,'2024-05-02 04:45:33.000000',48,'2023-08-09 13:10:20.000000',1,'E80B7D64-ADBA-5B63-998E-BD1F22BB6522','TQE27VGK8KO','EXPIRED','MOBILE'),(3,20,'2024-12-25 20:01:16.000000',49,'2023-12-03 02:22:18.000000',2,'8B83DA86-E1C7-1B2B-8D4A-1D721704481D','EWH52WEW1LA','READY','ACCOUNT_TRANSFER'),(1,5,'2025-06-07 11:02:08.000000',50,'2024-12-19 15:35:35.000000',2,'32C5A991-BDA8-EAC6-A063-D2CF98831466','ELC81JNZ8XI','IN_PROGRESS','VIRTUAL_ACCOUNT'); -/*!40000 ALTER TABLE `payment` ENABLE KEYS */; -UNLOCK TABLES; diff --git a/src/test/java/org/chzz/market/MarketApplicationTests.java b/src/test/java/org/chzz/market/MarketApplicationTests.java index 5c811d0b..86b62614 100644 --- a/src/test/java/org/chzz/market/MarketApplicationTests.java +++ b/src/test/java/org/chzz/market/MarketApplicationTests.java @@ -1,9 +1,12 @@ package org.chzz.market; +import org.chzz.market.common.AWSConfig; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; @SpringBootTest +@Import(AWSConfig.class) class MarketApplicationTests { @Test diff --git a/src/test/java/org/chzz/market/common/AWSConfig.java b/src/test/java/org/chzz/market/common/AWSConfig.java index 441837fb..0dad0cd4 100644 --- a/src/test/java/org/chzz/market/common/AWSConfig.java +++ b/src/test/java/org/chzz/market/common/AWSConfig.java @@ -1,23 +1,28 @@ package org.chzz.market.common; -import com.amazonaws.auth.AWSCredentialsProvider; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ListObjectsV2Request; +import com.amazonaws.services.s3.model.ListObjectsV2Result; import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; + @TestConfiguration public class AWSConfig { - @Bean - @Primary - public AmazonS3 amazonS3() { - return Mockito.mock(AmazonS3.class); - } @Bean @Primary - public AWSCredentialsProvider awsCredentialsProvider() { - return Mockito.mock(AWSCredentialsProvider.class); + public AmazonS3 amazonS3() { + AmazonS3 s3 = Mockito.mock(AmazonS3.class); + ListObjectsV2Result result = new ListObjectsV2Result(); + result.setPrefix("auction"); + result.setPrefix("profile"); + when(s3.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(result); + return s3; } } diff --git a/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java b/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java index 34e81125..2bbcd600 100644 --- a/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java +++ b/src/test/java/org/chzz/market/domain/auction/controller/AuctionControllerTest.java @@ -2,69 +2,52 @@ import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; import org.chzz.market.domain.auction.dto.AuctionRegisterType; import org.chzz.market.domain.auction.dto.request.RegisterRequest; import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.image.service.ImageService; +import org.chzz.market.domain.image.service.ObjectKeyValidator; import org.chzz.market.util.AuthenticatedRequestTest; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; class AuctionControllerTest extends AuthenticatedRequestTest { @MockBean - ImageService imageService; - - RegisterRequest request; - - MockMultipartFile image1, image2, image3, image4, image5, image6; - MockMultipartFile requestPart; - - @BeforeEach - void setUp() throws JsonProcessingException { - request = new RegisterRequest("name", "description", Category.BOOKS_AND_MEDIA, 10000, - AuctionRegisterType.PRE_REGISTER); - requestPart = new MockMultipartFile( - "request", "request", "application/json", objectMapper.writeValueAsBytes(request) - ); - - image1 = new MockMultipartFile("images", "imagefile1.jpeg", "image/jpeg", - "<>".getBytes()); - image2 = new MockMultipartFile("images", "imagefile2.jpeg", "image/jpeg", - "<>".getBytes()); - - image3 = new MockMultipartFile("images", "imagefile3.jpeg", "image/jpeg", - "<>".getBytes()); - image4 = new MockMultipartFile("images", "imagefile4.jpeg", "image/jpeg", - "<>".getBytes()); + ObjectKeyValidator objectKeyValidator; - image5 = new MockMultipartFile("images", "imagefile5.jpeg", "image/jpeg", - "<>".getBytes()); - image6 = new MockMultipartFile("images", "imagefile6.jpeg", "image/gif", - "<>".getBytes()); - } + @MockBean + ImageService imageService; @Test @DisplayName("사전 경매 등록") void testPreAuctionRegistration() throws Exception { + // given + RegisterRequest request = new RegisterRequest( + "name", + "description", + Category.BOOKS_AND_MEDIA, + 10000, + AuctionRegisterType.PRE_REGISTER, + List.of("A","B","C")); + String req = objectMapper.writeValueAsString(request); + doNothing().when(objectKeyValidator).validate(anyString()); + // when - mockMvc.perform(multipart("/api/v1/auctions") - .file(requestPart) - .file(image1) - .file(image2) - .contentType(MediaType.MULTIPART_FORM_DATA) - .accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/api/v1/auctions") + .content(req) + .contentType(MediaType.APPLICATION_JSON)) // then .andExpect(status().isCreated()) .andDo(print()); @@ -75,19 +58,23 @@ void testPreAuctionRegistration() throws Exception { @Test @DisplayName("이미지가 없는 경우") void testRegisterAuctionWithNoImage() throws Exception { + // given + RegisterRequest request = new RegisterRequest( + "name", + "description", + Category.BOOKS_AND_MEDIA, + 10000, + AuctionRegisterType.PRE_REGISTER, + null); + String req = objectMapper.writeValueAsString(request); - MockMultipartFile emptyImage = new MockMultipartFile( - "images", "file", MediaType.MULTIPART_FORM_DATA_VALUE, new byte[0] - ); // when - mockMvc.perform(multipart("/api/v1/auctions") - .file(emptyImage) - .file(requestPart) - .contentType(MediaType.MULTIPART_FORM_DATA) - .accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/api/v1/auctions") + .content(req) + .contentType(MediaType.APPLICATION_JSON)) // then .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message[0]").value(containsString("images: 파일은 최소 하나 이상 필요합니다."))) + .andExpect(jsonPath("$.message[0]").value(containsString("파일은 최소 하나 이상 필요합니다."))) .andDo(print()); } @@ -96,18 +83,21 @@ void testRegisterAuctionWithNoImage() throws Exception { @DisplayName("이미지가 5개 이상인 경우") void testRegisterAuctionWithOverImageCount() throws Exception { // given - mockMvc.perform(multipart("/api/v1/auctions") - .file(requestPart) - .file(image1) - .file(image2) - .file(image3) - .file(image4) - .file(image5) - .file(image6) - .accept(MediaType.APPLICATION_JSON)) + RegisterRequest request = new RegisterRequest( + "name", + "description", + Category.BOOKS_AND_MEDIA, + 10000, + AuctionRegisterType.PRE_REGISTER, + List.of("A","B","C","D","E","F")); + String req = objectMapper.writeValueAsString(request); + + mockMvc.perform(post("/api/v1/auctions") + .content(req) + .contentType(MediaType.APPLICATION_JSON)) // then .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message[0]").value(containsString("images: 이미지는 5장 이내로만 업로드 가능합니다."))) + .andExpect(jsonPath("$.message[0]").value(containsString("objectKeys: 이미지는 5장 이내로만 업로드 가능합니다."))) .andDo(print()); } } diff --git a/src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java index cf4536dd..27bbfaa5 100644 --- a/src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/auction/repository/AuctionQueryRepositoryTest.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.auction.dto.response.EndedAuctionResponse; import org.chzz.market.domain.auction.dto.response.LostAuctionResponse; import org.chzz.market.domain.auction.dto.response.OfficialAuctionDetailResponse; @@ -33,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -41,6 +43,7 @@ @SpringBootTest @Transactional +@Import(AWSConfig.class) class AuctionQueryRepositoryTest { @Autowired private AuctionQueryRepository auctionQueryRepository; @@ -269,7 +272,7 @@ class Auctions { //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(result.getContent().get(0).getIsSeller()).isTrue(); } @@ -286,7 +289,7 @@ class Auctions { //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(result.getContent().get(0).getIsSeller()).isFalse(); assertThat(result.getContent().get(0).getIsParticipated()).isFalse(); @@ -296,7 +299,7 @@ class Auctions { //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(result.getContent().get(0).getIsSeller()).isFalse(); assertThat(result.getContent().get(0).getIsParticipated()).isFalse(); } @@ -315,7 +318,7 @@ class Auctions { //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(result.getContent().get(0).getIsSeller()).isFalse(); assertThat(result.getContent().get(0).getIsParticipated()).isTrue(); } @@ -335,7 +338,7 @@ class Auctions { //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(result.getContent().get(0).getIsSeller()).isFalse(); assertThat(result.getContent().get(0).getIsLiked()).isTrue(); } @@ -353,7 +356,7 @@ class Auctions { assertThat(resultWithUserId).isNotNull(); assertThat(resultWithUserId.getContent()).hasSize(1); - assertThat(resultWithUserId.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(resultWithUserId.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(resultWithUserId.getContent().get(0).getIsSeller()).isFalse(); assertThat(resultWithUserId.getContent().get(0).getIsLiked()).isFalse(); @@ -363,7 +366,7 @@ class Auctions { assertThat(resultWithNull).isNotNull(); assertThat(resultWithNull.getContent()).hasSize(1); - assertThat(resultWithNull.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(resultWithNull.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); assertThat(resultWithNull.getContent().get(0).getIsSeller()).isFalse(); assertThat(resultWithNull.getContent().get(0).getIsLiked()).isFalse(); } @@ -387,8 +390,8 @@ class Auctions { //then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("아이패드"); // 가격이 더 높은 아이패드가 먼저 - assertThat(result.getContent().get(1).getProductName()).isEqualTo("맥북프로"); // 가격이 낮은 맥북프로가 나중 + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("아이패드"); // 가격이 더 높은 아이패드가 먼저 + assertThat(result.getContent().get(1).getAuctionName()).isEqualTo("맥북프로"); // 가격이 낮은 맥북프로가 나중 } @Test @@ -424,7 +427,7 @@ class Auctions { // then assertThat(resultWithin1Hour).isNotNull(); assertThat(resultWithin1Hour.getContent()).hasSize(1); - assertThat(resultWithin1Hour.getContent().get(0).getProductName()).isEqualTo("맥북프로"); + assertThat(resultWithin1Hour.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); // when - endWithinSeconds 2시간 이내 Page resultWithin2Hours = auctionQueryRepository.findOfficialAuctions( @@ -438,8 +441,8 @@ class Auctions { // then assertThat(resultWithin2Hours).isNotNull(); assertThat(resultWithin2Hours.getContent()).hasSize(2); - assertThat(resultWithin2Hours.getContent().get(0).getProductName()).isEqualTo("맥북프로"); // 더 빨리 종료되는 맥북 - assertThat(resultWithin2Hours.getContent().get(1).getProductName()).isEqualTo("아이패드"); // 나중에 종료되는 아이패드 + assertThat(resultWithin2Hours.getContent().get(0).getAuctionName()).isEqualTo("맥북프로"); // 더 빨리 종료되는 맥북 + assertThat(resultWithin2Hours.getContent().get(1).getAuctionName()).isEqualTo("아이패드"); // 나중에 종료되는 아이패드 } } @@ -467,8 +470,8 @@ class MyAuctions { // Then assertThat(result).isNotNull(); assertThat(result.getContent()).hasSize(2); - assertThat(result.getContent().get(0).getProductName()).isEqualTo("아이패드"); - assertThat(result.getContent().get(1).getProductName()).isEqualTo("맥북프로"); + assertThat(result.getContent().get(0).getAuctionName()).isEqualTo("아이패드"); + assertThat(result.getContent().get(1).getAuctionName()).isEqualTo("맥북프로"); } @@ -490,12 +493,12 @@ class MyAuctions { assertThat(result.getContent()).hasSize(2); PreAuctionResponse response1 = result.getContent().get(0); - assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getAuctionName()).isEqualTo("아이패드"); assertThat(response1.getIsSeller()).isTrue(); assertThat(response1.getIsLiked()).isFalse(); PreAuctionResponse response2 = result.getContent().get(1); - assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getAuctionName()).isEqualTo("맥북프로"); assertThat(response2.getIsSeller()).isTrue(); assertThat(response2.getIsLiked()).isFalse(); } @@ -523,11 +526,11 @@ class MyAuctions { assertThat(result.getContent()).hasSize(2); ProceedingAuctionResponse response1 = result.getContent().get(0); - assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getAuctionName()).isEqualTo("아이패드"); assertThat(response1.getIsSeller()).isTrue(); ProceedingAuctionResponse response2 = result.getContent().get(1); - assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getAuctionName()).isEqualTo("맥북프로"); assertThat(response2.getIsSeller()).isTrue(); } @@ -556,13 +559,13 @@ class MyAuctions { assertThat(result.getContent()).hasSize(2); EndedAuctionResponse response1 = result.getContent().get(0); - assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getAuctionName()).isEqualTo("아이패드"); assertThat(response1.getIsSeller()).isTrue(); assertThat(response1.getIsWon()).isTrue(); assertThat(response1.getIsOrdered()).isTrue(); EndedAuctionResponse response2 = result.getContent().get(1); - assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getAuctionName()).isEqualTo("맥북프로"); assertThat(response2.getIsSeller()).isTrue(); assertThat(response2.getIsWon()).isFalse(); assertThat(response2.getIsOrdered()).isFalse(); @@ -594,12 +597,12 @@ class MyAuctions { assertThat(result.getContent()).hasSize(2); WonAuctionResponse response1 = result.getContent().get(0); - assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getAuctionName()).isEqualTo("아이패드"); assertThat(response1.getIsOrdered()).isFalse(); assertThat(response1.getWinningAmount()).isEqualTo(3000L); WonAuctionResponse response2 = result.getContent().get(1); - assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getAuctionName()).isEqualTo("맥북프로"); assertThat(response2.getIsOrdered()).isTrue(); assertThat(response2.getWinningAmount()).isEqualTo(2000L); } @@ -630,10 +633,10 @@ class MyAuctions { assertThat(result.getContent()).hasSize(2); LostAuctionResponse response1 = result.getContent().get(0); - assertThat(response1.getProductName()).isEqualTo("아이패드"); + assertThat(response1.getAuctionName()).isEqualTo("아이패드"); LostAuctionResponse response2 = result.getContent().get(1); - assertThat(response2.getProductName()).isEqualTo("맥북프로"); + assertThat(response2.getAuctionName()).isEqualTo("맥북프로"); } diff --git a/src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java b/src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java index 239163c4..c959fb9e 100644 --- a/src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java +++ b/src/test/java/org/chzz/market/domain/auction/service/AuctionLookupServiceTest.java @@ -3,15 +3,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.chzz.market.domain.auction.error.AuctionErrorCode.END_WITHIN_MINUTES_PARAM_ALLOWED_FOR_PROCEEDING_ONLY; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.auction.error.AuctionException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.PageRequest; @SpringBootTest +@Import(AWSConfig.class) class AuctionLookupServiceTest { @Autowired AuctionLookupService auctionLookupService; diff --git a/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java b/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java index 7c39f531..f400673b 100644 --- a/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java +++ b/src/test/java/org/chzz/market/domain/bid/repository/BidQueryRepositoryTest.java @@ -4,8 +4,9 @@ import java.util.Comparator; import java.util.List; -import org.chzz.market.domain.auction.entity.AuctionStatus; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.auction.entity.Auction; +import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Category; import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.bid.dto.response.BidInfoResponse; @@ -17,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -25,6 +27,7 @@ @SpringBootTest @Transactional +@Import(AWSConfig.class) class BidQueryRepositoryTest { @Autowired AuctionRepository auctionRepository; diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java index 8080dae2..851dfe02 100644 --- a/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java +++ b/src/test/java/org/chzz/market/domain/bid/service/BidCancelLockServiceTest.java @@ -11,6 +11,7 @@ import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Auction; import org.chzz.market.domain.auction.entity.Category; @@ -26,8 +27,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; @SpringBootTest +@Import(AWSConfig.class) class BidCancelLockServiceTest { @Autowired diff --git a/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java b/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java index da724efc..e4d7116a 100644 --- a/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java +++ b/src/test/java/org/chzz/market/domain/bid/service/BidCreateServiceConcurrencyTest.java @@ -11,6 +11,7 @@ import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.auction.entity.Auction; import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Category; @@ -25,8 +26,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; @SpringBootTest +@Import(AWSConfig.class) public class BidCreateServiceConcurrencyTest { @Autowired diff --git a/src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java b/src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java index c82a58df..0828cf22 100644 --- a/src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java +++ b/src/test/java/org/chzz/market/domain/like/service/LikeUpdateServiceConcurrencyTest.java @@ -5,6 +5,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.auction.entity.AuctionStatus; import org.chzz.market.domain.auction.entity.Auction; import org.chzz.market.domain.auction.entity.Category; @@ -16,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; @SpringBootTest +@Import(AWSConfig.class) public class LikeUpdateServiceConcurrencyTest { @Autowired private UserRepository userRepository; diff --git a/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java b/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java index cd8f1851..bb6c2f1d 100644 --- a/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java +++ b/src/test/java/org/chzz/market/domain/user/service/UserServiceTest.java @@ -10,7 +10,6 @@ import static org.mockito.Mockito.when; import java.util.Optional; -import org.chzz.market.domain.image.service.ImageService; import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest; import org.chzz.market.domain.user.dto.request.UserCreateRequest; import org.chzz.market.domain.user.dto.response.NicknameAvailabilityResponse; @@ -28,14 +27,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; -@ExtendWith(MockitoExtension.class) + +@ExtendWith({MockitoExtension.class}) class UserServiceTest { @Mock private UserRepository userRepository; - @Mock - private ImageService imageService; + @InjectMocks private UserService userService; @@ -67,7 +66,10 @@ void setUp() { updateUserProfileRequest = UpdateUserProfileRequest.builder() .nickname("수정된 닉네임") .bio("수정된 자기 소개") + .objectKey("auction/image.jpg") .build(); + + ReflectionTestUtils.setField(userService, "cloudfrontDomain", "https://cdn.example.com/test"); } @Nested @@ -197,7 +199,7 @@ void updateUserProfile_Success() { when(userRepository.findByNickname(any())).thenReturn(Optional.empty()); // when - userService.updateUserProfile(user1.getId(), null, updateUserProfileRequest); + userService.updateUserProfile(user1.getId(), updateUserProfileRequest); // then assertThat(user1.getNickname()).isEqualTo("수정된 닉네임"); @@ -211,22 +213,15 @@ void updateUserProfile_Success_WithFile() { when(userRepository.findById(any())).thenReturn(Optional.of(user1)); when(userRepository.findByNickname(any())).thenReturn(Optional.empty()); - MockMultipartFile file = new MockMultipartFile( - "profileImage", - "image.jpg", - "image/jpeg", - "test image content".getBytes() - ); - - when(imageService.uploadImage(file)).thenReturn("https://cdn.example.com/image.jpg"); +// when(imageService.uploadImage(file)).thenReturn("https://cdn.example.com/image.jpg"); // when - userService.updateUserProfile(user1.getId(), file, updateUserProfileRequest); + userService.updateUserProfile(user1.getId(), updateUserProfileRequest); // then assertThat(user1.getNickname()).isEqualTo("수정된 닉네임"); assertThat(user1.getBio()).isEqualTo("수정된 자기 소개"); - assertThat(user1.getProfileImageUrl()).isEqualTo("https://cdn.example.com/image.jpg"); + assertThat(user1.getProfileImageUrl()).isEqualTo("https://cdn.example.com/test/auction/image.jpg"); } @Test @@ -243,7 +238,7 @@ void updateUserProfile_WithExistingImage_SetToDefaultImage() { .build(); // when - userService.updateUserProfile(user3.getId(), null, request); + userService.updateUserProfile(user3.getId(), request); // then assertThat(user3.getProfileImageUrl()).isNull(); // 기본 이미지로 변경 시 URL은 null @@ -258,25 +253,19 @@ void updateUserProfile_WithExistingImage_UploadNewImage() { when(userRepository.findById(any())).thenReturn(Optional.of(user3)); when(userRepository.findByNickname(any())).thenReturn(Optional.empty()); - MockMultipartFile file = new MockMultipartFile( - "profileImage", - "image.jpg", - "image/jpeg", - "test image content".getBytes() - ); - - when(imageService.uploadImage(file)).thenReturn("https://cdn.example.com/new-image.jpg"); +// when(imageService.uploadImage(file)).thenReturn("https://cdn.example.com/new-image.jpg"); UpdateUserProfileRequest request = UpdateUserProfileRequest.builder() .nickname("수정된 닉네임") .bio("수정된 자기 소개") + .objectKey("new-image.jpg") .build(); // when - userService.updateUserProfile(user3.getId(), file, request); + userService.updateUserProfile(user3.getId(), request); // then - assertThat(user3.getProfileImageUrl()).isEqualTo("https://cdn.example.com/new-image.jpg"); + assertThat(user3.getProfileImageUrl()).isEqualTo("https://cdn.example.com/test/new-image.jpg"); assertThat(user3.getNickname()).isEqualTo("수정된 닉네임"); assertThat(user3.getBio()).isEqualTo("수정된 자기 소개"); } @@ -289,7 +278,7 @@ void updateUserProfile_Fail_UserNotFound() { // when, then assertThrows(UserException.class, () -> - userService.updateUserProfile(999L, null, updateUserProfileRequest) + userService.updateUserProfile(999L, updateUserProfileRequest) ); } } diff --git a/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java b/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java index 321ae570..d7933b5f 100644 --- a/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java +++ b/src/test/java/org/chzz/market/util/AuthenticatedRequestTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.UUID; +import org.chzz.market.common.AWSConfig; import org.chzz.market.domain.user.dto.CustomUserDetails; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.repository.UserRepository; @@ -10,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -18,6 +20,7 @@ @AutoConfigureMockMvc @SpringBootTest +@Import(AWSConfig.class) public class AuthenticatedRequestTest { @Autowired protected MockMvc mockMvc; From efcf600c4433b115a1d85af6f13a80a5d52bab13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=98=88=EC=B0=AC?= <88381563+YeaChan05@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:23:47 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20UUID=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 경매 이미지 업로드시 UUID를 각 이미지에 별도의 UUID 생성하도록 수정 Co-authored-by: Jun Choi <121853214+junest66@users.noreply.github.com> --- .../chzz/market/domain/image/service/ImageUploadService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java b/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java index f2d248c9..75dbab0d 100644 --- a/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java +++ b/src/main/java/org/chzz/market/domain/image/service/ImageUploadService.java @@ -31,10 +31,10 @@ public CreatePresignedUrlResponse createPresignedUrl(BucketPrefix bucketPrefix, public List createAuctionPresignedUrls(final List requests) { Date expiration = getPreSignedUrlExpiration(); - String fileId = UUID.randomUUID().toString();//하니의 경매가 동일한 fileId를 갖음 String name = BucketPrefix.AUCTION.getName(); return requests.stream() .map(fileName -> { + String fileId = UUID.randomUUID().toString(); String objectKey = String.format("%s/%s/%s", name, fileId, fileName.hashCode());//실제로 파일명은 해시값으로 구분 GeneratePresignedUrlRequest request = getGeneratePreSignedUrlRequest(objectKey, expiration); URL url = amazonS3.generatePresignedUrl(request);