diff --git a/.github/workflows/pr-title-and-branch-validation.yml b/.github/workflows/pr-title-and-branch-validation.yml index 4556dd25..f1d116c6 100644 --- a/.github/workflows/pr-title-and-branch-validation.yml +++ b/.github/workflows/pr-title-and-branch-validation.yml @@ -2,7 +2,7 @@ name: PR Title and Branch Name Validation on: pull_request: - types: [ opened, synchronize] + types: [ opened, synchronize, edited] branches: - main - develop @@ -12,7 +12,7 @@ on: jobs: validate-title-and-branch: runs-on: ubuntu-latest - if: ${{ !(startsWith(github.head_ref, 'release/') && github.base_ref == 'develop') }} # release/* -> develop일 때는 실행하지 않음 + if: ${{ !(startsWith(github.head_ref, 'release/') && github.base_ref == 'develop') && github.actor != 'jira' }} # release/* -> develop일 때는 실행하지 않음, 또한 Jira Bot이 실행하는 경우 무시 steps: - name: Check out code uses: actions/checkout@v3 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index e30531bf..731bda86 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -13,22 +13,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Create Tag - run: | - BRANCH_NAME="${{ github.event.pull_request.head.ref }}" - # 'release/' 또는 'hotfix/' 접두사를 제거 - TAG_NAME="${BRANCH_NAME#release/}" - TAG_NAME="${TAG_NAME#hotfix/}" - # 태그 이름이 비어있을 경우 처리 - if [ -z "$TAG_NAME" ]; then - echo "Error: TAG_NAME is empty. Please check the branch name." - exit 1 - fi - git tag $TAG_NAME - git push origin $TAG_NAME - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Update Release uses: release-drafter/release-drafter@v5 with: diff --git a/compose-local.yaml b/compose-local.yaml index 87c11537..b896106b 100644 --- a/compose-local.yaml +++ b/compose-local.yaml @@ -14,8 +14,7 @@ services: ports: - "3306:3306" volumes: - - ./mysql/data:/var/lib/mysql - - ./mysql/conf.d:/etc/mysql/conf.d + - chzz-mysql-data:/var/lib/mysql command: - "mysqld" - "--character-set-server=utf8mb4" @@ -34,48 +33,51 @@ services: ports: - "3000:3000" - node-exporter: - image: prom/node-exporter:latest - container_name: node-exporter - restart: unless-stopped - volumes: - - /proc:/host/proc:ro - - /sys:/host/sys:ro - - /:/rootfs:ro - command: - - '--path.procfs=/host/proc' - - '--path.rootfs=/rootfs' - - '--path.sysfs=/host/sys' - - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' - ports: - - "9100:9100" - - prometheus: - image: prom/prometheus:latest - container_name: prometheus - volumes: - - ./monitoring/prometheus:/etc/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - ports: - - "9090:9090" +# node-exporter: +# image: prom/node-exporter:latest +# container_name: node-exporter +# restart: unless-stopped +# volumes: +# - /proc:/host/proc:ro +# - /sys:/host/sys:ro +# - /:/rootfs:ro +# command: +# - '--path.procfs=/host/proc' +# - '--path.rootfs=/rootfs' +# - '--path.sysfs=/host/sys' +# - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' +# ports: +# - "9100:9100" +# +# prometheus: +# image: prom/prometheus:latest +# container_name: prometheus +# volumes: +# - ./monitoring/prometheus:/etc/prometheus +# command: +# - '--config.file=/etc/prometheus/prometheus.yml' +# ports: +# - "9090:9090" +# +# grafana: +# image: grafana/grafana:latest +# container_name: grafana +# volumes: +# - ./monitoring/grafana:/var/lib/grafana +# environment: +# - GF_SECURITY_ADMIN_PASSWORD=admin +# ports: +# - "3001:3000" +# +# loki: +# image: grafana/loki:latest +# container_name: loki +# ports: +# - "3100:3100" +# volumes: +# - ./monitoring/loki:/etc/loki +# - ./monitoring/loki-data:/tmp/loki +# command: -config.file=/etc/loki/loki-config.yaml - grafana: - image: grafana/grafana:latest - container_name: grafana - volumes: - - ./monitoring/grafana:/var/lib/grafana - environment: - - GF_SECURITY_ADMIN_PASSWORD=admin - ports: - - "3001:3000" - - loki: - image: grafana/loki:latest - container_name: loki - ports: - - "3100:3100" - volumes: - - ./monitoring/loki:/etc/loki - - ./monitoring/loki-data:/tmp/loki - command: -config.file=/etc/loki/loki-config.yaml +volumes: + chzz-mysql-data: diff --git a/nginx.conf b/nginx.conf index c5874ca8..0a8ff7db 100644 --- a/nginx.conf +++ b/nginx.conf @@ -64,6 +64,8 @@ include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + client_max_body_size 50M; # 최대 파일 업로드 크기 50MB로 설정 + location / { proxy_pass http://app; proxy_set_header Host $host; diff --git a/src/main/java/org/chzz/market/common/error/ErrorResponse.java b/src/main/java/org/chzz/market/common/error/ErrorResponse.java index 3c63194e..1adf61f8 100644 --- a/src/main/java/org/chzz/market/common/error/ErrorResponse.java +++ b/src/main/java/org/chzz/market/common/error/ErrorResponse.java @@ -3,17 +3,20 @@ import lombok.Builder; @Builder -public record ErrorResponse(String message, +public record ErrorResponse(String name, + String[] message, int status) { public static ErrorResponse from(final ErrorCode errorCode) { return ErrorResponse.builder() + .name(errorCode.name()) .status(errorCode.getHttpStatus().value()) - .message(errorCode.getMessage()) + .message(new String[]{errorCode.getMessage()}) .build(); } - public static ErrorResponse of(final ErrorCode errorCode, final String detailedErrorMessage) { + public static ErrorResponse of(final ErrorCode errorCode, final String[] detailedErrorMessage) { return ErrorResponse.builder() + .name(errorCode.name()) .status(errorCode.getHttpStatus().value()) .message(detailedErrorMessage) .build(); 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 bf30036e..69e189a9 100644 --- a/src/main/java/org/chzz/market/common/error/GlobalErrorCode.java +++ b/src/main/java/org/chzz/market/common/error/GlobalErrorCode.java @@ -10,6 +10,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 name of parameter included"), + VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "Validation failed"), AUTHENTICATION_REQUIRED(HttpStatus.UNAUTHORIZED, "Authentication is required"), ACCESS_DENIED(HttpStatus.FORBIDDEN, "Access is denied"), UNSUPPORTED_SORT_TYPE(HttpStatus.BAD_REQUEST,"Unsupported type of sort" ), 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 778d8c88..f16e54d3 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,7 +3,7 @@ import static org.chzz.market.common.error.GlobalErrorCode.EXTERNAL_API_ERROR; -import java.util.stream.Collectors; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.chzz.market.common.error.ErrorCode; import org.chzz.market.common.error.ErrorResponse; @@ -14,7 +14,6 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.lang.Nullable; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -31,6 +30,40 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private static final String LOG_FORMAT = "\nException Class = {}\nResponse Code = {}\nMessage = {}"; + // 1. 커스텀 예외 핸들러 + + /** + * 비즈니스 로직에서 정의한 예외가 발생할 때 처리 + */ + @ExceptionHandler(BusinessException.class) + protected ResponseEntity handleBusinessException(BusinessException e) { + ErrorCode errorCode = e.getErrorCode(); + logException(e, errorCode); + return handleExceptionInternal(errorCode); + } + + /** + * 시스템 전역에서 발생하는 예외를 처리 + */ + @ExceptionHandler(GlobalException.class) + protected ResponseEntity handleGlobalException(GlobalException e) { + GlobalErrorCode errorCode = e.getErrorCode(); + logException(e, errorCode); + return handleExceptionInternal(errorCode); + } + + /** + * 외부 API 호출 시 발생하는 WebClient 관련 예외 처리 + */ + @ExceptionHandler(WebClientResponseException.class) + protected ResponseEntity handleWebClientResponseException(final WebClientResponseException exception) { + logException(exception, EXTERNAL_API_ERROR, exception.getResponseBodyAsString()); + return handleExceptionInternal(EXTERNAL_API_ERROR); + } + + /** + * 요청 매개변수의 타입이 잘못된 경우 발생 + */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { GlobalErrorCode errorCode = GlobalErrorCode.UNSUPPORTED_PARAMETER_TYPE; @@ -38,85 +71,78 @@ protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgu return handleExceptionInternal(errorCode); } - @Nullable + // 2. ResponseEntityExceptionHandler에서 오버라이드된 핸들러 + + /** + * 필수 요청 매개변수가 누락된 경우 발생 + */ @Override protected ResponseEntity handleMissingServletRequestParameter( - final MissingServletRequestParameterException e, - final HttpHeaders headers, - final HttpStatusCode status, - final WebRequest request) { + @NonNull final MissingServletRequestParameterException e, + @NonNull final HttpHeaders headers, + @NonNull final HttpStatusCode status, + @NonNull final WebRequest request) { GlobalErrorCode errorCode = GlobalErrorCode.UNSUPPORTED_PARAMETER_NAME; logException(e, errorCode); return handleExceptionInternal(errorCode); } + /** + * 존재하지 않는 URL 요청 시 발생 + */ @Override - protected ResponseEntity handleNoHandlerFoundException(final NoHandlerFoundException e, - final HttpHeaders headers, - final HttpStatusCode status, - final WebRequest request) { + protected ResponseEntity handleNoHandlerFoundException(@NonNull final NoHandlerFoundException e, + @NonNull final HttpHeaders headers, + @NonNull final HttpStatusCode status, + @NonNull final WebRequest request) { final GlobalErrorCode errorCode = GlobalErrorCode.RESOURCE_NOT_FOUND; logException(e, errorCode); return handleExceptionInternal(errorCode); } - @Nullable + /** + * 멀티파트 요청의 필수 파트가 누락된 경우 발생 + */ @Override - protected ResponseEntity handleMissingServletRequestPart(MissingServletRequestPartException ex, - HttpHeaders headers, HttpStatusCode status, - WebRequest request) { + protected ResponseEntity handleMissingServletRequestPart(@NonNull MissingServletRequestPartException ex, + @NonNull HttpHeaders headers, + @NonNull HttpStatusCode status, + @NonNull WebRequest request) { final GlobalErrorCode errorCode = GlobalErrorCode.INVALID_REQUEST_PARAMETER; logException(ex, errorCode); return handleExceptionInternal(errorCode); } /** - * @param e 비즈니스 로직상 발생한 커스텀 예외 - * @return 커스텀 예외 메세지와 상태를 담은 {@link ResponseEntity} + * 요청의 유효성 검사 실패 시 발생 */ - @ExceptionHandler(BusinessException.class) - protected ResponseEntity handleBusinessException(BusinessException e) { - ErrorCode errorCode = e.getErrorCode(); - logException(e, errorCode); - return handleExceptionInternal(errorCode); - } - - @ExceptionHandler(GlobalException.class) - protected ResponseEntity handleGlobalException(GlobalException e) { - GlobalErrorCode errorCode = e.getErrorCode(); - logException(e, errorCode); - return handleExceptionInternal(errorCode); - } - - @ExceptionHandler(WebClientResponseException.class) - protected ResponseEntity handleWebClientResponseException(final WebClientResponseException exception) { - logException(exception, EXTERNAL_API_ERROR, exception.getResponseBodyAsString()); - return handleExceptionInternal(EXTERNAL_API_ERROR); - } - @Override - protected ResponseEntity handleMethodArgumentNotValid( - MethodArgumentNotValidException ex, - HttpHeaders headers, - HttpStatusCode status, - WebRequest request) { + protected ResponseEntity handleMethodArgumentNotValid(@NonNull MethodArgumentNotValidException ex, + @NonNull HttpHeaders headers, + @NonNull HttpStatusCode status, + @NonNull WebRequest request) { - GlobalErrorCode errorCode = GlobalErrorCode.INVALID_REQUEST_PARAMETER; - String detailedErrorMessage = ex.getBindingResult().getFieldErrors() + GlobalErrorCode errorCode = GlobalErrorCode.VALIDATION_FAILED; + String[] detailedErrorMessages = ex.getBindingResult().getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) - .collect(Collectors.joining("; ")); - logException(ex, errorCode, detailedErrorMessage); + .toArray(String[]::new); + logException(ex, errorCode, detailedErrorMessages); - ErrorResponse errorResponse = ErrorResponse.of(errorCode, detailedErrorMessage); + ErrorResponse errorResponse = ErrorResponse.of(errorCode, detailedErrorMessages); return ResponseEntity .status(errorCode.getHttpStatus()) .body(errorResponse); } + /** + * 읽을 수 없는 HTTP 메시지가 수신된 경우 발생 + */ @Override - protected ResponseEntity handleHttpMessageNotReadable( - HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + protected ResponseEntity handleHttpMessageNotReadable(@NonNull HttpMessageNotReadableException ex, + @NonNull HttpHeaders headers, + @NonNull HttpStatusCode status, + @NonNull WebRequest request) { GlobalErrorCode errorCode = GlobalErrorCode.INVALID_REQUEST_PARAMETER; logException(ex, errorCode, ex.getMessage()); @@ -127,12 +153,20 @@ protected ResponseEntity handleHttpMessageNotReadable( .body(errorResponse); } + /** + * 예외 처리 응답 생성 메서드 + */ private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity .status(errorCode.getHttpStatus()) .body(ErrorResponse.from(errorCode)); } + // 3. 예외 로깅 메서드들 + + /** + * 예외 정보를 로깅 (ErrorCode 메시지 사용) + */ private void logException(final Exception e, final ErrorCode errorCode) { log.error(LOG_FORMAT, e.getClass(), @@ -140,10 +174,23 @@ private void logException(final Exception e, final ErrorCode errorCode) { errorCode.getMessage()); } + /** + * 예외 정보를 로깅 (단일 메시지 사용) + */ private void logException(final Exception e, final ErrorCode errorCode, final String message) { log.error(LOG_FORMAT, e.getClass(), errorCode.getHttpStatus().value(), message); } + + /** + * 예외 정보를 로깅 (다중 메시지 사용) + */ + private void logException(final Exception e, final ErrorCode errorCode, final String[] message) { + log.error(LOG_FORMAT, + e.getClass(), + errorCode.getHttpStatus().value(), + String.join("; ", message)); + } } diff --git a/src/main/java/org/chzz/market/common/error/response/ErrorResponse.java b/src/main/java/org/chzz/market/common/error/response/ErrorResponse.java deleted file mode 100644 index 99c810bc..00000000 --- a/src/main/java/org/chzz/market/common/error/response/ErrorResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.chzz.market.common.error.response; - -import lombok.Builder; -import org.chzz.market.common.error.ErrorCode; - -@Builder -public record ErrorResponse(String code, String message, int status) { - public static ErrorResponse from(final ErrorCode errorCode){ - return ErrorResponse.builder() - .status(errorCode.getHttpStatus().value()) - .message(errorCode.getMessage()) - .build(); - } -} 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 95b939f4..53638a1b 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,18 +3,22 @@ import jakarta.validation.Valid; 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.*; +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.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.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.AuctionViewType; import org.chzz.market.domain.bid.service.BidService; import org.chzz.market.domain.product.entity.Product.Category; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -31,17 +35,16 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/auctions") public class AuctionController { - private static final Logger logger = LoggerFactory.getLogger(AuctionController.class); - private final AuctionService auctionService; private final BidService bidService; private final AuctionRegistrationServiceFactory registrationServiceFactory; - /* + /** * 경매 목록 조회 */ @GetMapping @@ -51,21 +54,25 @@ public ResponseEntity getAuctionList(@RequestParam Category category, return ResponseEntity.ok(auctionService.getAuctionListByCategory(category, userId, pageable)); } - /* - * 경매 상세 조회 (simple 일 경우 간단 정보만 조회) + /** + * Best 경매 상품 목록 조회 */ - @GetMapping("/{auctionId}") - public ResponseEntity getAuctionDetails( - @PathVariable Long auctionId, - @RequestParam(defaultValue="FULL") AuctionViewType viewType, - @LoginUser Long userId) { - return switch (viewType) { - case FULL -> ResponseEntity.ok(auctionService.getFullAuctionDetails(auctionId, userId)); - case SIMPLE -> ResponseEntity.ok(auctionService.getSimpleAuctionDetails(auctionId)); - }; + @GetMapping("/best") + public ResponseEntity bestAuctionList() { + List bestAuctionList = auctionService.getBestAuctionList(); + return ResponseEntity.ok(bestAuctionList); } - /* + /** + * Imminent 경매 상품 목록 조회 + */ + @GetMapping("/imminent") + public ResponseEntity imminentAuctionList() { + List imminentAuctionList = auctionService.getImminentAuctionList(); + return ResponseEntity.ok(imminentAuctionList); + } + + /** * 경매 입찰 내역 조회 */ @GetMapping("/history") @@ -73,7 +80,7 @@ public ResponseEntity getAuctionHistory(@LoginUser Long userId, Pageable page return ResponseEntity.ok(auctionService.getAuctionHistory(userId, pageable)); } - /* + /** * 내가 성공한 경매 조회 */ @GetMapping("/won") @@ -83,7 +90,7 @@ public ResponseEntity> getWonAuctionHistory( return ResponseEntity.ok(auctionService.getWonAuctionHistory(userId, pageable)); } - /* + /** * 내가 실패한 경매 조회 */ @GetMapping("/lost") @@ -93,34 +100,39 @@ public ResponseEntity> getLostAuctionHistory( return ResponseEntity.ok(auctionService.getLostAuctionHistory(userId, pageable)); } - /* - * 경매 등록 + /** + * 경매 상세 조회 (simple 일 경우 간단 정보만 조회) */ - @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", required = true) List images) { - - AuctionRegistrationService auctionRegistrationService = registrationServiceFactory.getService( - request.getAuctionRegisterType()); - RegisterResponse response = auctionRegistrationService.register(userId, request, images); + @GetMapping("/{auctionId}") + public ResponseEntity getAuctionDetails( + @PathVariable Long auctionId, + @RequestParam(defaultValue = "FULL") AuctionViewType type, + @LoginUser Long userId) { + return switch (type) { + case FULL -> ResponseEntity.ok(auctionService.getFullAuctionDetails(auctionId, userId)); + case SIMPLE -> ResponseEntity.ok(auctionService.getSimpleAuctionDetails(auctionId)); + }; + } - return ResponseEntity.status(HttpStatus.CREATED).body(response); + /** + * 경매 입찰 목록 조회 + */ + @GetMapping("/{auctionId}/bids") + public ResponseEntity getBids(@LoginUser Long userId, @PathVariable Long auctionId, Pageable pageable) { + return ResponseEntity.ok(bidService.getBidsByAuctionId(userId, auctionId, pageable)); } - /* - * 경매 상품으로 전환 + /** + * 사용자 경매 상품 목록 조회 (토큰) */ - @PostMapping("/start") - public ResponseEntity startAuction(@LoginUser Long userId, @RequestBody @Valid StartAuctionRequest request) { - StartAuctionResponse response = auctionService.startAuction(userId, request); - logger.info("경매 상품으로 성공적으로 전환되었습니다. 상품 ID: {}", response.productId()); - return ResponseEntity.status(HttpStatus.CREATED).body(response); + @GetMapping("/users") + public ResponseEntity getUserRegisteredAuction(@LoginUser Long userId, + Pageable pageable) { + return ResponseEntity.ok(auctionService.getAuctionListByUserId(userId, pageable)); } - /* - * 사용자 경매 상품 목록 조회 + /** + * 사용자 경매 상품 목록 조회 (닉네임) */ @GetMapping("/users/{nickname}") public ResponseEntity> getUserAuctionList(@PathVariable String nickname, @@ -128,29 +140,30 @@ public ResponseEntity> getUserAuctionList(@PathVariabl return ResponseEntity.ok(auctionService.getAuctionListByNickname(nickname, pageable)); } - /* - * Best 경매 상품 목록 조회 + /** + * 경매 등록 */ - @GetMapping("/best") - public ResponseEntity bestAuctionList() { - List bestAuctionList=auctionService.getBestAuctionList(); - return ResponseEntity.ok(bestAuctionList); - } + @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) { - /* - * Imminent 경매 상품 목록 조회 - */ - @GetMapping("/imminent") - public ResponseEntity imminentAuctionList() { - List imminentAuctionList = auctionService.getImminentAuctionList(); - return ResponseEntity.ok(imminentAuctionList); + AuctionRegistrationService auctionRegistrationService = registrationServiceFactory.getService( + request.getAuctionRegisterType()); + RegisterResponse response = auctionRegistrationService.register(userId, request, images); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); } - /* - * 경매 입찰 목록 조회 + /** + * 경매 상품으로 전환 */ - @GetMapping("/{auctionId}/bids") - public ResponseEntity getBids(@LoginUser Long userId, @PathVariable Long auctionId, Pageable pageable) { - return ResponseEntity.ok(bidService.getBidsByAuctionId(userId, auctionId, pageable)); + @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); } } 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 index 87760819..e3fdfa8b 100644 --- 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 @@ -12,6 +12,7 @@ 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 @@ -37,7 +38,7 @@ public abstract class BaseRegisterRequest { protected Category category; @NotNull - @Min(value = 1000, message = "시작 가격은 최소 1,000원 이상, 1000의 배수이어야 합니다") + @ThousandMultiple protected Integer minPrice; @NotNull(message = "경매 타입을 선택해주세요") 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 7b94b8b2..b9609319 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,14 +1,25 @@ package org.chzz.market.domain.auction.entity; -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.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.ENDED; +import static org.chzz.market.domain.auction.type.AuctionStatus.PROCEEDING; -import jakarta.persistence.*; - -import java.time.LocalDateTime; +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.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; @@ -17,8 +28,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.chzz.market.domain.auction.entity.listener.AuctionEntityListener; -import org.chzz.market.domain.auction.type.AuctionStatus; 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.bid.entity.Bid; import org.chzz.market.domain.product.entity.Product; 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 index d7190fa1..a9020339 100644 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustom.java +++ b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustom.java @@ -5,6 +5,7 @@ import org.chzz.market.domain.auction.dto.response.*; 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; @@ -52,6 +53,13 @@ public interface AuctionRepositoryCustom { */ Page findAuctionsByNickname(String nickname, Pageable pageable); + /** + * @param userId 사용자 ID + * @param pageable 페이징 정보 + * @return 페이징된 사용자 경매 등록 기록 + */ + Page findAuctionsByUserId(Long userId, Pageable pageable); + /** * 홈 화면의 베스트 경매 조회 * @return 입찰 기록이 많은 경매 정보 @@ -64,13 +72,6 @@ public interface AuctionRepositoryCustom { */ List findImminentAuctions(); - /** - * 사용자의 참여 횟수, 낙찰 횟수, 낙찰 실패 횟수를 조회합니다. - * @param userId 사용자 ID - * @return 참여 횟수, 낙찰 횟수, 낙찰 실패 횟수 응답 - */ - List getAuctionParticipations(Long userId); - /** * 사용자가 낙찰한 경매 이력을 조회합니다. * @param userId 사용자 ID @@ -86,4 +87,10 @@ public interface AuctionRepositoryCustom { * @return 페이징된 낙찰 실패 경매 응답 리스트 */ Page findLostAuctionHistoryByUserId(Long userId, Pageable pageable); + + /** + * @param userId - 사용자 ID + * @return 사용자가 참여한 상태별 경매들의 수 + */ + ParticipationCountsResponse getParticipationCounts(Long userId); } diff --git a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryImpl.java b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java similarity index 74% rename from src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryImpl.java rename to src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java index 10829b50..23ce7583 100644 --- a/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryImpl.java +++ b/src/main/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImpl.java @@ -2,7 +2,7 @@ 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.AuctionRepositoryImpl.AuctionOrder.POPULARITY; +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; @@ -13,14 +13,17 @@ 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; @@ -29,15 +32,27 @@ 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.*; +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.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.WonAuctionResponse; import org.chzz.market.domain.image.entity.QImage; 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 AuctionRepositoryImpl implements AuctionRepositoryCustom { +public class AuctionRepositoryCustomImpl implements AuctionRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; private final QuerydslOrderProvider querydslOrderProvider; @@ -88,12 +103,8 @@ public Page findAuctionsByCategory(Category category, Long user */ @Override public Page findParticipatingAuctionRecord(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) - .join(auction.bids, bid) - .join(auction.product, product) - .on(bid.bidder.id.eq(userId)) - .where(bid.status.eq(ACTIVE)); + JPAQuery baseQuery = getActualParticipatedAuction(userId) + .join(auction.product, product); List content = baseQuery .select(new QAuctionResponse( @@ -155,25 +166,26 @@ public Optional findAuctionDetailsById(Long auctionId, L /** * 경매 ID와 사용자 ID로 경매 간단 상세 정보를 조회합니다. + * * @param auctionId 경매 ID - * @return 경매 간단 상세정보 응답 + * @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.id.eq(product.id).and(image.id.eq(getFirstImageId()))) - .leftJoin(bid).on(bid.auction.id.eq(auctionId).and(bid.status.eq(ACTIVE))) - .where(auction.id.eq(auctionId)) - .groupBy(product.name, image.cdnPath, product.minPrice) - .fetchOne()); + .select(new QSimpleAuctionResponse( + image.cdnPath, + product.name, + product.minPrice, + bid.countDistinct() + )) + .from(auction) + .join(auction.product, product) + .leftJoin(image).on(image.product.id.eq(product.id).and(image.id.eq(getFirstImageId()))) + .leftJoin(bid).on(bid.auction.id.eq(auctionId).and(bid.status.eq(ACTIVE))) + .where(auction.id.eq(auctionId)) + .groupBy(product.name, image.cdnPath, product.minPrice) + .fetchOne()); } /** @@ -190,7 +202,28 @@ public Page findAuctionsByNickname(String nickname, Pageabl .join(product.user, user) .where(user.nickname.eq(nickname)); - List content = baseQuery + 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, @@ -199,7 +232,9 @@ public Page findAuctionsByNickname(String nickname, Pageabl product.minPrice.longValue(), getBidCount(), auction.status, - auction.createdAt)) + auction.createdAt)); + + List content = contentQuery .leftJoin(image).on(image.product.id.eq(product.id) .and(image.id.eq(getFirstImageId()))) .orderBy(querydslOrderProvider.getOrderSpecifiers(pageable)) @@ -207,9 +242,7 @@ public Page findAuctionsByNickname(String nickname, Pageabl .limit(pageable.getPageSize()) .fetch(); - JPAQuery countQuery = baseQuery - .select(auction.count()); - + JPAQuery countQuery = baseQuery.select(auction.count()); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); } @@ -243,7 +276,8 @@ public List findBestAuctions() { /** * 홈 화면의 임박 경매 조회 - * @return 경매 종료까지 1시간 이내인 경매 정보 + * + * @return 경매 종료까지 1시간 이내인 경매 정보 */ @Override public List findImminentAuctions() { @@ -280,12 +314,9 @@ public List findImminentAuctions() { */ @Override public Page findWonAuctionHistoryByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) + JPAQuery baseQuery = getActualParticipatedAuction(userId) .join(auction.product, product) - .join(product.user, user) - .join(bid).on(bid.auction.eq(auction) - .and(bid.status.ne(CANCELLED).and(bid.bidder.id.eq(userId)))) +// .join(product.user, user)//?? 안쓰는데 .where(auction.winnerId.eq(userId) .and(auction.status.eq(ENDED))); @@ -320,14 +351,10 @@ public Page findWonAuctionHistoryByUserId(Long userId, Pagea */ @Override public Page findLostAuctionHistoryByUserId(Long userId, Pageable pageable) { - JPAQuery baseQuery = jpaQueryFactory - .from(auction) + JPAQuery baseQuery = getActualParticipatedAuction(userId) .join(auction.product, product) - .join(bid).on(bid.auction.eq(auction) - .and(bid.bidder.id.eq(userId)) - .and(bid.status.ne(CANCELLED))) - .where(auction.winnerId.ne(userId).or(auction.winnerId.isNull()) - .and(auction.status.eq(ENDED))); + .where(auction.winnerId.ne(userId) + .or(auction.winnerId.isNull().and(auction.status.eq(ENDED)))); List content = baseQuery .select(new QLostAuctionResponse( @@ -351,88 +378,70 @@ public Page findLostAuctionHistoryByUserId(Long userId, Pag return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); } - /** - * 사용자의 참여 횟수, 낙찰 횟수, 낙찰 실패 횟수를 조회합니다. - * @param userId 사용자 ID - * @return 참여 횟수, 낙찰 횟수, 낙찰 실패 횟수 응답 - */ @Override - public List getAuctionParticipations(Long userId) { - return jpaQueryFactory - .select(new QAuctionParticipationResponse( - auction.status, - auction.winnerId, - auction.id.countDistinct() - )) + 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(auction.bids, bid) - .where(bid.bidder.id.eq(userId).and(bid.status.ne(CANCELLED))) - .groupBy(auction.status, auction.winnerId) + .where(bid.bidder.id.eq(userId) + .and(bid.status.eq(ACTIVE))) .fetch(); - } - - /** - * 사용자가 참여 중인 경매 수를 조회합니다. - * - * @param userId 사용자 ID - * @return 참여 중인 경매 수 - */ -// private JPQLQuery getOngoingAuctionCount(Long userId) { -// return JPAExpressions -// .select(auction.id.count()) -// .from(auction) -// .join(auction.bids, bid) -// .where(auction.status.eq(PROCEEDING) -// .and(bid.bidder.id.eq(userId)) -// .and(bid.status.ne(BidStatus.CANCELLED))); -// } - /** - * 사용자의 낙찰 성공 경매 수를 조회합니다. - * - * @param userId 사용자 ID - * @return 낙찰 경매 수 - */ -// private JPQLQuery getSuccessfulAuctionCount(Long userId) { -// return JPAExpressions -// .select(auction.id.count()) -// .from(auction) -// .where(auction.status.eq(ENDED) -// .and(auction.winnerId.eq(userId))); -// } + BooleanExpression isParticipatedAuction = auction.id.in(participatedAuctionIds); - /** - * 사용자의 낙찰 실패 경매 수를 조회합니다. - * - * @param userId 사용자 ID - * @return 낙찰 실패 경매 수 - */ -// private JPQLQuery getFailedAuctionCount(Long userId) { -// return JPAExpressions -// .select(auction.id.count()) -// .from(auction) -// .join(auction.bids, bid) -// .where(auction.status.eq(ENDED) -// .and(auction.winnerId.ne(userId)) -// .and(bid.bidder.id.eq(userId)) -// .and(bid.status.ne(BidStatus.CANCELLED))); -// } + 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 + ); + } /** - * 사용자의 낙찰 취소 경매 수를 조회합니다. - * - * @param userId 사용자 ID - * @return 낙찰 취소 경매 수 + * @param userId 사용자 pk + * @return 실제 사용자가 참여한 경매(취소된 입찰 제외) */ -// private JPQLQuery getEndedAuctionCount(Long userId) { -// return JPAExpressions -// .select(auction.countDistinct()) -// .from(auction) -// .join(auction.bids, bid) -// .where(auction.status.eq(ENDED) -// .and(bid.bidder.id.eq(userId)) -// .and(bid.status.ne(BidStatus.CANCELLED))); -// } + private JPAQuery getActualParticipatedAuction(Long userId) { + return jpaQueryFactory + .from(auction) + .join(auction.bids, bid) + .on(bid.bidder.id.eq(userId) + .and(bid.status.eq(ACTIVE))); + } /** * 상품의 첫 번째 이미지를 조회합니다. 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 index 80b7ae5e..aa062ab9 100644 --- a/src/main/java/org/chzz/market/domain/auction/service/AuctionService.java +++ b/src/main/java/org/chzz/market/domain/auction/service/AuctionService.java @@ -242,4 +242,8 @@ private void processNonWinningBids(List bids, String productName, Image fir AUCTION_NON_WINNER.getMessage(productName), firstImage)); // 미낙찰자 알림 이벤트 } } + + public Page getAuctionListByUserId(Long userId, Pageable pageable) { + return auctionRepository.findAuctionsByUserId(userId, pageable); + } } 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 18f228fb..394f5b94 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 @@ -26,13 +26,9 @@ public class BidController { private final BidService bidService; - @PostMapping - public ResponseEntity createBid(@Valid @RequestBody BidCreateRequest bidCreateRequest, - @LoginUser Long userId) { - bidService.createBid(bidCreateRequest, userId); - return ResponseEntity.status(CREATED).build(); - } - + /** + * 입찰 내역 조회 + */ @GetMapping public ResponseEntity findUsersBidHistory( @LoginUser Long userId, @@ -42,6 +38,19 @@ public ResponseEntity findUsersBidHistory( return ResponseEntity.ok(records); } + /** + * 입찰 요청 및 수정 + */ + @PostMapping + public ResponseEntity createBid(@Valid @RequestBody BidCreateRequest bidCreateRequest, + @LoginUser Long userId) { + bidService.createBid(bidCreateRequest, userId); + return ResponseEntity.status(CREATED).build(); + } + + /** + * 입찰 취소 + */ @PatchMapping("/{bidId}/cancel") public ResponseEntity cancelBid(@PathVariable Long bidId, @LoginUser Long userId) { 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 d9f41409..ec3f078c 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 @@ -55,9 +55,9 @@ public class Bid extends BaseTimeEntity { private Long amount; @Column(nullable = false) - @ColumnDefault(value = "3") + @ColumnDefault(value = "2") @Builder.Default - private int count = 3; + private int count = 2; @Enumerated(EnumType.STRING) @Column(columnDefinition = "varchar(255)") 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 2498bd94..e1605329 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,7 +10,8 @@ public enum ImageErrorCode implements ErrorCode { IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드를 실패했습니다."), IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제를 실패했습니다. "), - IMAGE_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 저장을 실패했습니다."); + IMAGE_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 저장을 실패했습니다."), + INVALID_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 확장자입니다."); private final HttpStatus httpStatus; private final String message; 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 941d3aef..6d89a0ac 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,29 +1,32 @@ 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.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; 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.image.repository.ImageRepository; import org.chzz.market.domain.product.entity.Product; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; -import java.util.List; -import java.util.stream.Collectors; - +@Slf4j @Service @RequiredArgsConstructor public class ImageService { - - private static final Logger logger = LoggerFactory.getLogger(ImageService.class); - private final ImageUploader imageUploader; private final ImageRepository imageRepository; private final AmazonS3 amazonS3Client; @@ -42,7 +45,7 @@ public List uploadImages(List images) { .map(this::uploadImage) .toList(); - uploadedUrls.forEach(url -> logger.info("업로드 된 이미지 : {}", getFullImageUrl(url))); + uploadedUrls.forEach(url -> log.info("업로드 된 이미지 : {}", cloudfrontDomain + "/" + url)); return uploadedUrls; } @@ -51,7 +54,9 @@ public List uploadImages(List images) { * 단일 이미지 파일 업로드 및 CDN 경로 리스트 반환 */ private String uploadImage(MultipartFile image) { - return imageUploader.uploadImage(image); + String uniqueFileName = createUniqueFileName(Objects.requireNonNull(image.getOriginalFilename())); + + return imageUploader.uploadImage(image, uniqueFileName); } /** @@ -61,7 +66,7 @@ private String uploadImage(MultipartFile image) { public List saveProductImageEntities(Product product, List cdnPaths) { List images = cdnPaths.stream() .map(cdnPath -> Image.builder() - .cdnPath(cdnPath) + .cdnPath(cloudfrontDomain + "/" + cdnPath) .product(product) .build()) .toList(); @@ -73,8 +78,8 @@ public List saveProductImageEntities(Product product, List cdnPat /** * 업로드된 이미지 삭제 */ - public void deleteUploadImages(List cdnPaths) { - cdnPaths.forEach(this::deleteImage); + public void deleteUploadImages(List fullImageUrls) { + fullImageUrls.forEach(this::deleteImage); } /** @@ -82,19 +87,37 @@ public void deleteUploadImages(List cdnPaths) { */ private void deleteImage(String cdnPath) { try { - String key = cdnPath.substring(1); + 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 e) { - throw new ImageException(ImageErrorCode.IMAGE_DELETE_FAILED); + } catch (AmazonServiceException | MalformedURLException e) { + throw new ImageException(IMAGE_DELETE_FAILED); } } /** - * CDN 경로로부터 전체 이미지 URL 재구성 - * 이미지 -> 서버에 들어왔는지 확인하는 로그에 사용 + * 고유한 파일 이름 생성 + */ + 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; + } + + /** + * 파일 확장자 검증 */ - public String getFullImageUrl(String cdnPath) { - return "https://" + cloudfrontDomain + cdnPath; + private boolean isValidFileExtension(String extension) { + List allowedExtensions = Arrays.asList("jpg", "jpeg", "png", "webp"); + return allowedExtensions.contains(extension.toLowerCase()); } } 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 index 8081c0d3..63bbec5e 100644 --- a/src/main/java/org/chzz/market/domain/image/service/ImageUploader.java +++ b/src/main/java/org/chzz/market/domain/image/service/ImageUploader.java @@ -6,6 +6,6 @@ 테스트 이미지 업로드 인터페이스 */ public interface ImageUploader { - String uploadImage(MultipartFile image); + 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 0e652b72..184c0b16 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 @@ -2,6 +2,7 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; +import java.io.IOException; import lombok.RequiredArgsConstructor; import org.chzz.market.domain.image.error.ImageErrorCode; import org.chzz.market.domain.image.error.exception.ImageException; @@ -9,8 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - @Service @RequiredArgsConstructor public class S3ImageUploader implements ImageUploader { @@ -20,8 +19,7 @@ public class S3ImageUploader implements ImageUploader { private String bucket; @Override - public String uploadImage(MultipartFile image) { - String fileName = image.getOriginalFilename(); + public String uploadImage(MultipartFile image, String fileName) { try { ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(image.getSize()); @@ -29,7 +27,7 @@ public String uploadImage(MultipartFile image) { amazonS3Client.putObject(bucket, fileName, image.getInputStream(), metadata); - return "/" + fileName; // CDN 경로 생성 (전체 URL 아닌 경로만) + return fileName; // CDN 경로 생성 (전체 URL 아닌 경로만) } catch (IOException e) { throw new ImageException(ImageErrorCode.IMAGE_UPLOAD_FAILED); } diff --git a/src/main/java/org/chzz/market/domain/notification/controller/NotificationController.java b/src/main/java/org/chzz/market/domain/notification/controller/NotificationController.java index eb884d1e..c0fa660a 100644 --- a/src/main/java/org/chzz/market/domain/notification/controller/NotificationController.java +++ b/src/main/java/org/chzz/market/domain/notification/controller/NotificationController.java @@ -27,6 +27,12 @@ public ResponseEntity getNotifications(@LoginUser Long userId, Pageable pagea return ResponseEntity.ok(notificationService.getNotifications(userId, pageable)); } + @GetMapping(value = "/subscribe", produces = TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribe(@LoginUser Long userId, HttpServletResponse response) { + response.setHeader("X-Accel-Buffering", "no"); + return notificationService.subscribe(userId); + } + @PostMapping("/{notificationId}/read") public ResponseEntity readNotification(@LoginUser Long userId, @PathVariable Long notificationId) { notificationService.readNotification(userId, notificationId); @@ -38,10 +44,4 @@ public ResponseEntity deleteNotification(@LoginUser Long userId, @PathVariabl notificationService.deleteNotification(userId, notificationId); return ResponseEntity.ok().build(); } - - @GetMapping(value = "/subscribe", produces = TEXT_EVENT_STREAM_VALUE) - public SseEmitter subscribe(@LoginUser Long userId, HttpServletResponse response) { - response.setHeader("X-Accel-Buffering", "no"); - return notificationService.subscribe(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 index 7e945e36..285cf697 100644 --- a/src/main/java/org/chzz/market/domain/product/controller/ProductController.java +++ b/src/main/java/org/chzz/market/domain/product/controller/ProductController.java @@ -1,8 +1,11 @@ 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 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; @@ -13,8 +16,6 @@ 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.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -31,14 +32,12 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import static org.chzz.market.domain.product.entity.Product.*; +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/products") public class ProductController { - private static final Logger logger = LoggerFactory.getLogger(ProductController.class); - private final ProductService productService; private final LikeService likeService; @@ -79,7 +78,14 @@ public ResponseEntity getProductDetails( public ResponseEntity> getMyProductList( @PathVariable String nickname, Pageable pageable) { - return ResponseEntity.ok(productService.getMyProductList(nickname, pageable)); + return ResponseEntity.ok(productService.getProductListByNickname(nickname, pageable)); + } + + @GetMapping("/users") + public ResponseEntity> getRegisteredProductList( + @LoginUser Long userId, + Pageable pageable) { + return ResponseEntity.ok(productService.getProductListByUserId(userId, pageable)); } /* @@ -113,7 +119,7 @@ public ResponseEntity deleteProduct( @PathVariable Long productId, @LoginUser Long userId) { DeleteProductResponse response = productService.deleteProduct(productId, userId); - logger.info("상품이 성공적으로 삭제되었습니다. 상품 ID: {}", productId); + log.info("상품이 성공적으로 삭제되었습니다. 상품 ID: {}", productId); return ResponseEntity.status(HttpStatus.OK).body(response); } 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 index e55596a3..91048845 100644 --- a/src/main/java/org/chzz/market/domain/product/entity/Product.java +++ b/src/main/java/org/chzz/market/domain/product/entity/Product.java @@ -57,7 +57,6 @@ public class Product extends BaseTimeEntity { private String description; @Column - @ThousandMultiple private Integer minPrice; @Column(nullable = false, columnDefinition = "varchar(30)") 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 index 7670ad31..d1570bc9 100644 --- a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustom.java +++ b/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryCustom.java @@ -1,14 +1,13 @@ 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.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.Optional; - -import static org.chzz.market.domain.product.entity.Product.*; - public interface ProductRepositoryCustom { /** * 카테고리와 정렬 조건에 따라 사전 등록 상품 리스트를 조회합니다. @@ -35,6 +34,13 @@ public interface ProductRepositoryCustom { */ Page findProductsByNickname(String nickname, Pageable pageable); + /** + * 사용자 인증정보를 통해 사용자가 등록한 상품 리스트 조회 + * @param userId 사용자 ID + * @param pageable 페이징 정보 + * @return + */ + Page findProductsByUserId(Long userId, Pageable pageable); /** * 사용자 ID에 따라 사용자가 참여한 사전 경매 리스트를 조회합니다. * @param userId 사용자 ID diff --git a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryImpl.java b/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryImpl.java index 0163eb3c..667bacc2 100644 --- a/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryImpl.java +++ b/src/main/java/org/chzz/market/domain/product/repository/ProductRepositoryImpl.java @@ -78,7 +78,7 @@ public Page findProductsByCategory(Category category, Long user * * @param productId 상품 ID * @param userId 사용자 ID - * @return 상품 상세 정보 + * @return 상품 상세 정보 */ @Override public Optional findProductDetailsById(Long productId, Long userId) { @@ -127,6 +127,20 @@ public Page findProductsByNickname(String nickname, Pageable pa .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) + .join(auction.product, product) + .on(user.id.eq(product.id)); + + return getProductResponses(pageable, baseQuery); + } + + private Page getProductResponses(Pageable pageable, JPAQuery baseQuery) { List content = baseQuery .select(new QProductResponse( product.id, 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 index 2bd32936..1e4cc0d5 100644 --- a/src/main/java/org/chzz/market/domain/product/service/ProductService.java +++ b/src/main/java/org/chzz/market/domain/product/service/ProductService.java @@ -66,10 +66,14 @@ public ProductDetailsResponse getProductDetails(Long productId, Long userId) { /** * 나의 사전 등록 상품 목록 조회 */ - public Page getMyProductList(String nickname, Pageable pageable) { + public Page getProductListByNickname(String nickname, Pageable pageable) { return productRepository.findProductsByNickname(nickname, pageable); } + public Page getProductListByUserId(Long userId, Pageable pageable) { + return productRepository.findProductsByUserId(userId, pageable); + } + /** * 내가 참여한 사전경매 조회 */ 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 c9a06c0b..e0a2ae06 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 @@ -1,19 +1,17 @@ package org.chzz.market.domain.user.service; -import static org.chzz.market.domain.auction.type.AuctionStatus.*; import static org.chzz.market.domain.user.error.UserErrorCode.NICKNAME_DUPLICATION; import static org.chzz.market.domain.user.error.UserErrorCode.USER_NOT_FOUND; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.chzz.market.domain.auction.dto.response.AuctionParticipationResponse; import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.dto.response.UpdateProfileResponse; 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.ParticipationCountsResponse; +import org.chzz.market.domain.user.dto.response.UpdateProfileResponse; import org.chzz.market.domain.user.dto.response.UserProfileResponse; import org.chzz.market.domain.user.entity.User; import org.chzz.market.domain.user.error.exception.UserException; @@ -21,8 +19,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor @@ -112,42 +108,11 @@ private UserProfileResponse getUserProfileInternal(User user) { long preRegisterCount = productRepository.countPreRegisteredProductsByUserId(user.getId()); long registeredAuctionCount = auctionRepository.countByProductUserId(user.getId()); - ParticipationCountsResponse counts = new ParticipationCountsResponse( - user.getOngoingAuctionCount(), - user.getSuccessfulBidCount(), - user.getFailedBidCount() - ); + ParticipationCountsResponse counts = auctionRepository.getParticipationCounts(user.getId()); return UserProfileResponse.of(user, counts, preRegisterCount, registeredAuctionCount); } - /* - * 경매 참여 횟수 계산 - */ - private ParticipationCountsResponse calculateParticipationCounts(Long userId, List participations) { - long ongoingAuctionCount = 0; - long successfulBidCount = 0; - long failedBidCount = 0; - - for (AuctionParticipationResponse participation : participations) { - if (participation.status() == PROCEEDING) { - ongoingAuctionCount += participation.count(); - } else { - if (userId.equals(participation.winnerId())) { - successfulBidCount += participation.count(); - } else { - failedBidCount += participation.count(); - } - } - } - - return new ParticipationCountsResponse( - ongoingAuctionCount, - successfulBidCount, - failedBidCount - ); - } - /* * 닉네임으로 사용자 조회 */ diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 7a4f40d7..4e257655 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -1,27 +1,227 @@ --- MySQL dump 10.13 Distrib 8.4.0, for Linux (aarch64) --- --- Host: localhost Database: chzzdb --- ------------------------------------------------------ --- Server version 8.4.0 +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` +( + `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; + +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` +( + `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; + +DROP TABLE IF EXISTS `auction`; +CREATE TABLE `auction` +( + `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; + +DROP TABLE IF EXISTS `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; + +DROP TABLE IF EXISTS `notification`; +CREATE TABLE `notification` +( + `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; + +DROP TABLE IF EXISTS `address`; +CREATE TABLE `address` +( + `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; + +DROP TABLE IF EXISTS `bank_account`; +CREATE TABLE `bank_account` +( + `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; + +DROP TABLE IF EXISTS `bid`; +CREATE TABLE `bid` +( + `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; + +DROP TABLE IF EXISTS `like_table`; +CREATE TABLE `like_table` +( + `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; -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!50503 SET NAMES utf8mb4 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +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; --- --- Table structure for table `QRTZ_BLOB_TRIGGERS` --- +# ------------------------------------------------------------------------------------------------------------------------ + +DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; +CREATE TABLE `QRTZ_JOB_DETAILS` +( + `SCHED_NAME` varchar(120) NOT NULL, + `JOB_NAME` varchar(190) NOT NULL, + `JOB_GROUP` varchar(190) NOT NULL, + `DESCRIPTION` varchar(250) DEFAULT NULL, + `JOB_CLASS_NAME` varchar(250) NOT NULL, + `IS_DURABLE` varchar(1) NOT NULL, + `IS_NONCONCURRENT` varchar(1) NOT NULL, + `IS_UPDATE_DATA` varchar(1) NOT NULL, + `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; + +DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; +CREATE TABLE `QRTZ_TRIGGERS` +( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(190) NOT NULL, + `TRIGGER_GROUP` varchar(190) NOT NULL, + `JOB_NAME` varchar(190) NOT NULL, + `JOB_GROUP` varchar(190) NOT NULL, + `DESCRIPTION` varchar(250) DEFAULT NULL, + `NEXT_FIRE_TIME` bigint DEFAULT NULL, + `PREV_FIRE_TIME` bigint DEFAULT NULL, + `PRIORITY` int DEFAULT NULL, + `TRIGGER_STATE` varchar(16) NOT NULL, + `TRIGGER_TYPE` varchar(8) NOT NULL, + `START_TIME` bigint NOT NULL, + `END_TIME` bigint DEFAULT NULL, + `CALENDAR_NAME` varchar(190) DEFAULT NULL, + `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`), + 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; DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_BLOB_TRIGGERS` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -32,15 +232,8 @@ CREATE TABLE `QRTZ_BLOB_TRIGGERS` 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_CALENDARS` --- DROP TABLE IF EXISTS `QRTZ_CALENDARS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_CALENDARS` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -48,15 +241,8 @@ CREATE TABLE `QRTZ_CALENDARS` `CALENDAR` blob NOT NULL, PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_CRON_TRIGGERS` --- DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_CRON_TRIGGERS` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -67,15 +253,8 @@ CREATE TABLE `QRTZ_CRON_TRIGGERS` 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_FIRED_TRIGGERS` --- DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_FIRED_TRIGGERS` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -99,70 +278,24 @@ CREATE TABLE `QRTZ_FIRED_TRIGGERS` 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_JOB_DETAILS` --- - -DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `QRTZ_JOB_DETAILS` -( - `SCHED_NAME` varchar(120) NOT NULL, - `JOB_NAME` varchar(190) NOT NULL, - `JOB_GROUP` varchar(190) NOT NULL, - `DESCRIPTION` varchar(250) DEFAULT NULL, - `JOB_CLASS_NAME` varchar(250) NOT NULL, - `IS_DURABLE` varchar(1) NOT NULL, - `IS_NONCONCURRENT` varchar(1) NOT NULL, - `IS_UPDATE_DATA` varchar(1) NOT NULL, - `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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_LOCKS` --- DROP TABLE IF EXISTS `QRTZ_LOCKS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_PAUSED_TRIGGER_GRPS` --- DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_SCHEDULER_STATE` --- DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_SCHEDULER_STATE` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -171,15 +304,8 @@ CREATE TABLE `QRTZ_SCHEDULER_STATE` `CHECKIN_INTERVAL` bigint NOT NULL, PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_SIMPLE_TRIGGERS` --- DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -191,15 +317,8 @@ CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_SIMPROP_TRIGGERS` --- DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` ( `SCHED_NAME` varchar(120) NOT NULL, @@ -219,295 +338,3 @@ CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `QRTZ_TRIGGERS` --- - -DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `QRTZ_TRIGGERS` -( - `SCHED_NAME` varchar(120) NOT NULL, - `TRIGGER_NAME` varchar(190) NOT NULL, - `TRIGGER_GROUP` varchar(190) NOT NULL, - `JOB_NAME` varchar(190) NOT NULL, - `JOB_GROUP` varchar(190) NOT NULL, - `DESCRIPTION` varchar(250) DEFAULT NULL, - `NEXT_FIRE_TIME` bigint DEFAULT NULL, - `PREV_FIRE_TIME` bigint DEFAULT NULL, - `PRIORITY` int DEFAULT NULL, - `TRIGGER_STATE` varchar(16) NOT NULL, - `TRIGGER_TYPE` varchar(8) NOT NULL, - `START_TIME` bigint NOT NULL, - `END_TIME` bigint DEFAULT NULL, - `CALENDAR_NAME` varchar(190) DEFAULT NULL, - `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`), - 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `address` --- - -DROP TABLE IF EXISTS `address`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `address` -( - `address_id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `detail_address` varchar(255) DEFAULT NULL, - `jibun` varchar(255) DEFAULT NULL, - `road_address` varchar(255) DEFAULT NULL, - `zipcode` varchar(255) 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 -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `auction` --- - -DROP TABLE IF EXISTS `auction`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `auction` -( - `auction_id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime(6) DEFAULT NULL, - `end_date_time` datetime(6) DEFAULT NULL, - `product_id` bigint DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `winner_id` bigint DEFAULT NULL, - `status` varchar(20) 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `bank_account` --- - -DROP TABLE IF EXISTS `bank_account`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `bank_account` -( - `bank_account_id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `number` varchar(255) NOT NULL, - `name` varchar(255) NOT 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `bid` --- - -DROP TABLE IF EXISTS `bid`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `bid` -( - `count` int NOT NULL DEFAULT '3', - `amount` bigint NOT NULL, - `auction_id` bigint NOT NULL, - `bid_id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `status` varchar(255) 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `image` --- - -DROP TABLE IF EXISTS `image`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `image` -( - `created_at` datetime(6) DEFAULT NULL, - `image_id` bigint NOT NULL AUTO_INCREMENT, - `product_id` bigint DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `cdn_path` varchar(255) NOT 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `like_table` --- - -DROP TABLE IF EXISTS `like_table`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `like_table` -( - `created_at` datetime(6) DEFAULT NULL, - `like_id` bigint NOT NULL AUTO_INCREMENT, - `product_id` bigint NOT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `notification` --- - -DROP TABLE IF EXISTS `notification`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `notification` -( - `is_deleted` bit(1) NOT NULL, - `is_read` bit(1) NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `image_id` bigint DEFAULT NULL, - `notification_id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `message` varchar(255) NOT NULL, - `auction_id` bigint DEFAULT NULL, - `type` varchar(31) NOT 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `payment` --- - -DROP TABLE IF EXISTS `payment`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `payment` -( - `amount` bigint NOT NULL, - `auction_id` bigint NOT NULL, - `created_at` datetime(6) DEFAULT NULL, - `payment_id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `order_id` varchar(255) NOT NULL, - `payment_key` varchar(255) NOT NULL, - `method` varchar(30) NOT NULL, - `status` varchar(30) NOT 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `product` --- - -DROP TABLE IF EXISTS `product`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `product` -( - `min_price` int DEFAULT NULL, - `created_at` datetime(6) DEFAULT NULL, - `product_id` bigint NOT NULL AUTO_INCREMENT, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL, - `description` varchar(1000) DEFAULT NULL, - `name` varchar(255) NOT NULL, - `category` varchar(30) NOT 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; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `users` --- - -DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `users` -( - `created_at` datetime(6) DEFAULT NULL, - `updated_at` datetime(6) DEFAULT NULL, - `user_id` bigint NOT NULL AUTO_INCREMENT, - `customer_key` binary(16) NOT NULL, - `nickname` varchar(25) DEFAULT NULL, - `bio` text, - `email` varchar(255) NOT NULL, - `link` varchar(255) DEFAULT NULL, - `provider_id` varchar(255) NOT NULL, - `provider_type` varchar(20) DEFAULT NULL, - `user_role` varchar(20) 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 -/*!40101 SET character_set_client = @saved_cs_client */; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2024-09-08 7:28:08 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 new file mode 100644 index 00000000..13147a51 --- /dev/null +++ b/src/main/resources/db/migration/V2__update_default_count_value.sql @@ -0,0 +1,7 @@ +-- 파일명: 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 new file mode 100644 index 00000000..15d80688 --- /dev/null +++ b/src/main/resources/db/migration/V3__reorder_columns.sql @@ -0,0 +1,66 @@ +-- 파일명: 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/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryImplTest.java b/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java similarity index 74% rename from src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryImplTest.java rename to src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java index ad619850..da7d5211 100644 --- a/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryImplTest.java +++ b/src/test/java/org/chzz/market/domain/auction/repository/AuctionRepositoryCustomImplTest.java @@ -1,6 +1,7 @@ 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 java.time.LocalDateTime; @@ -21,36 +22,27 @@ 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.AfterEach; import org.junit.jupiter.api.BeforeAll; +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.transaction.annotation.Transactional; @DatabaseTest -class AuctionRepositoryImplTest { +class AuctionRepositoryCustomImplTest { @Autowired AuctionRepository auctionRepository; - @Autowired - ProductRepository productRepository; - - @Autowired - ImageRepository imageRepository; - - @Autowired - BidRepository bidRepository; - - @Autowired - UserRepository userRepository; - private static User user1, user2, user3, user4; private static Product product1, product2, product3, product4, product5, product6, product7; private static Auction auction1, auction2, auction3, auction4, auction5, auction6, auction7; @@ -102,7 +94,6 @@ static void setUpOnce(@Autowired UserRepository userRepository, .endDateTime(LocalDateTime.now().plusSeconds(700)).build(); auctionRepository.saveAll(List.of(auction1, auction2, auction3, auction4, auction5, auction6, auction7)); - image1 = Image.builder().product(product1).cdnPath("path/to/image1_1.jpg").build(); image2 = Image.builder().product(product1).cdnPath("path/to/image1_2.jpg").build(); image3 = Image.builder().product(product2).cdnPath("path/to/image2.jpg").build(); @@ -326,7 +317,7 @@ void testImminentAuctions() { // then assertThat(imminentAuctions).isNotEmpty(); assertThat(imminentAuctions.size()).isEqualTo(3); - assertThat(imminentAuctions).allMatch(auctionResponse ->auctionResponse.getTimeRemaining()<=3600); + assertThat(imminentAuctions).allMatch(auctionResponse -> auctionResponse.getTimeRemaining() <= 3600); assertThat(imminentAuctions).isSortedAccordingTo( Comparator.comparing(BaseAuctionDTO::getTimeRemaining)); @@ -370,4 +361,147 @@ void testFindParticipatingAuctionRecordWithTime() { assertThat(responses.getContent()).isSortedAccordingTo( Comparator.comparingLong(BaseAuctionDTO::getTimeRemaining)); } + + @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() + .bidder(user) + .auction(successedAuction) + .amount(10000L) + .build(); + Bid bid2 = Bid.builder() + .bidder(user) + .auction(failedAuction1) + .amount(10000L) + .build(); + Bid bid3 = Bid.builder() + .bidder(user) + .auction(failedAuction2) + .amount(10000L) + .build(); + Bid bid4 = Bid.builder() + .bidder(user) + .auction(ongoingAuction1) + .amount(1000L) + .build(); + Bid bid5 = Bid.builder() + .bidder(user) + .auction(ongoingAuction2) + .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); + } + } } 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 index c0664a2b..edf2710c 100644 --- a/src/test/java/org/chzz/market/domain/bid/service/BidServiceTest.java +++ b/src/test/java/org/chzz/market/domain/bid/service/BidServiceTest.java @@ -108,7 +108,7 @@ public void updateBid_Success() throws Exception { //then assertThat(bid.getId()).isEqualTo(1L); assertThat(bid.getAmount()).isEqualTo(2000L); - assertThat(bid.getCount()).isEqualTo(2L); + assertThat(bid.getCount()).isEqualTo(1L); } @Test 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 57fb0bbb..c368b237 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 @@ -46,8 +46,6 @@ void setUp() throws Exception { customUserDetails = mock(CustomUserDetails.class); when(authentication.getPrincipal()).thenReturn(customUserDetails); when(customUserDetails.getUser()).thenReturn(user); - - setPrivateField(customSuccessHandler, "clientUrl", "http://localhost:3000"); } private void setPrivateField(Object targetObject, String fieldName, String value) throws Exception { @@ -67,7 +65,6 @@ void onAuthenticationSuccess_WhenTempUser_RedirectToAdditionalInfoAndCreateTempT customSuccessHandler.onAuthenticationSuccess(request, response, authentication); // then - assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost:3000/form"); assertThat(response.getCookies()).hasSize(1); assertThat(response.getCookies()[0].getValue()).isEqualTo("temp-token"); verify(tokenService, times(1)).createTempToken(user); @@ -84,7 +81,6 @@ void onAuthenticationSuccess_WhenRegularUser_RedirectToMainAndCreateRefreshToken customSuccessHandler.onAuthenticationSuccess(request, response, authentication); // then - assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost:3000/login?status=success"); assertThat(response.getCookies()).hasSize(1); assertThat(response.getCookies()[0].getValue()).isEqualTo("refresh-token"); verify(tokenService, times(1)).createRefreshToken(user); 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 19993d9f..8b5a627a 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 @@ -2,14 +2,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +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.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import java.util.Optional; - import org.chzz.market.domain.auction.entity.Auction; import org.chzz.market.domain.auction.repository.AuctionRepository; import org.chzz.market.domain.auction.type.AuctionStatus; @@ -17,12 +22,11 @@ import org.chzz.market.domain.bid.entity.Bid; import org.chzz.market.domain.product.entity.Product; import org.chzz.market.domain.product.repository.ProductRepository; -import org.chzz.market.domain.user.dto.response.ParticipationCountsResponse; -import org.chzz.market.domain.user.dto.response.UserProfileResponse; +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.ParticipationCountsResponse; import org.chzz.market.domain.user.dto.response.UpdateProfileResponse; -import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest; 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; @@ -39,8 +43,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import static org.mockito.Mockito.verify; - @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock @@ -292,127 +294,4 @@ void updateUserProfile_Fail_UserNotFound() { ); } } - - @Nested - @DisplayName("사용자 정보 조회 테스트") - class getUserProfileTest { - @Test - @DisplayName("1. 사용자 정보 조회가 성공하는 경우") - public void getUserProfile_Success() { - // given - doReturn(5L).when(user1).getOngoingAuctionCount(); - doReturn(2L).when(user1).getSuccessfulBidCount(); - doReturn(1L).when(user1).getFailedBidCount(); - - when(userRepository.findByNickname("닉네임 1")).thenReturn(Optional.of(user1)); - when(productRepository.countPreRegisteredProductsByUserId(user1.getId())).thenReturn(2L); - when(auctionRepository.countByProductUserId(user1.getId())).thenReturn(2L); - // when - UserProfileResponse response = userService.getUserProfileByNickname("닉네임 1"); - - // then - assertNotNull(response); - assertEquals("닉네임 1", response.nickname()); - assertEquals("자기소개 1", response.bio()); - assertNotNull(response.participationCount()); - assertEquals(5L, response.participationCount().ongoingAuctionCount()); - assertEquals(2L, response.participationCount().successfulAuctionCount()); - assertEquals(1L, response.participationCount().failedAuctionCount()); - assertEquals(2L, response.preRegisterCount()); - assertEquals(2L, response.registeredAuctionCount()); - - verify(userRepository).findByNickname(user1.getNickname()); - verify(user1).getOngoingAuctionCount(); - verify(user1).getSuccessfulBidCount(); - verify(user1).getFailedBidCount(); - verify(productRepository).countPreRegisteredProductsByUserId(user1.getId()); - verify(auctionRepository).countByProductUserId(user1.getId()); - } - - @Test - @DisplayName("2. 존재하지 않는 사용자의 프로필 조회 시 예외 발생") - public void updateUser_UserNotFound() { - // given - when(userRepository.findByNickname("존재하지 않는 닉네임")).thenReturn(Optional.empty()); - - // when - assertThrows(UserException.class, () -> userService.getUserProfileByNickname("존재하지 않는 닉네임")); - verify(userRepository).findByNickname("존재하지 않는 닉네임"); - verifyNoInteractions(productRepository, auctionRepository); - } - - @Test - @DisplayName("3. 사용자의 경매 참여 카운트가 모두 0인 경우") - public void getUserProfile_ZeroCounts() { - // given - User zeroCountUser = spy(User.builder() - .id(3L) - .nickname("닉네임 3") - .bio("참여 없음") - .build()); - - doReturn(0L).when(zeroCountUser).getOngoingAuctionCount(); - doReturn(0L).when(zeroCountUser).getSuccessfulBidCount(); - doReturn(0L).when(zeroCountUser).getFailedBidCount(); - - when(userRepository.findByNickname("닉네임 3")).thenReturn(Optional.of(zeroCountUser)); - when(productRepository.countPreRegisteredProductsByUserId(zeroCountUser.getId())).thenReturn(0L); - when(auctionRepository.countByProductUserId(zeroCountUser.getId())).thenReturn(0L); - - // when - UserProfileResponse response = userService.getUserProfileByNickname("닉네임 3"); - - // then - assertNotNull(response); - assertEquals("닉네임 3", response.nickname()); - assertEquals("참여 없음", response.bio()); - assertNotNull(response.participationCount()); - assertEquals(0L, response.participationCount().ongoingAuctionCount()); - assertEquals(0L, response.participationCount().successfulAuctionCount()); - assertEquals(0L, response.participationCount().failedAuctionCount()); - assertEquals(0L, response.preRegisterCount()); - assertEquals(0L, response.registeredAuctionCount()); - - verify(userRepository).findByNickname(zeroCountUser.getNickname()); - verify(zeroCountUser).getOngoingAuctionCount(); - verify(zeroCountUser).getSuccessfulBidCount(); - verify(zeroCountUser).getFailedBidCount(); - verify(productRepository).countPreRegisteredProductsByUserId(3L); - verify(auctionRepository).countByProductUserId(3L); - } - - @Test - @DisplayName("4. 사전 등록 상품과 경매 상품만 있는 경우") - public void getUserProfile_WithPAndR() { - // given - doReturn(2L).when(user1).getOngoingAuctionCount(); - doReturn(3L).when(user1).getSuccessfulBidCount(); - doReturn(1L).when(user1).getFailedBidCount(); - - when(userRepository.findByNickname("닉네임 1")).thenReturn(Optional.of(user1)); - when(productRepository.countPreRegisteredProductsByUserId(user1.getId())).thenReturn(2L); - when(auctionRepository.countByProductUserId(user1.getId())).thenReturn(2L); - - // when - UserProfileResponse response = userService.getUserProfileByNickname("닉네임 1"); - - // then - assertNotNull(response); - assertEquals("닉네임 1", response.nickname()); - assertEquals("자기소개 1", response.bio()); - assertNotNull(response.participationCount()); - assertEquals(2L, response.participationCount().ongoingAuctionCount()); - assertEquals(3L, response.participationCount().successfulAuctionCount()); - assertEquals(1L, response.participationCount().failedAuctionCount()); - assertEquals(2L, response.preRegisterCount()); - assertEquals(2L, response.registeredAuctionCount()); - - verify(userRepository).findByNickname(user1.getNickname()); - verify(user1).getOngoingAuctionCount(); - verify(user1).getSuccessfulBidCount(); - verify(user1).getFailedBidCount(); - verify(productRepository).countPreRegisteredProductsByUserId(user1.getId()); - verify(auctionRepository).countByProductUserId(user1.getId()); - } - } }