Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {
implementation 'org.springframework.retry:spring-retry'
// AWS S3
implementation 'io.awspring.cloud:spring-cloud-starter-aws-secrets-manager-config:2.4.4'
// VAVR
implementation 'io.vavr:vavr:0.10.4'

/* DB */
// MySQL
Expand Down
28 changes: 20 additions & 8 deletions src/main/java/bc1/gream/domain/sell/provider/SellBidProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import bc1.gream.domain.sell.service.helper.deadline.Deadline;
import bc1.gream.domain.sell.service.helper.deadline.DeadlineCalculator;
import bc1.gream.domain.user.entity.User;
import bc1.gream.global.common.ResultCase;
import bc1.gream.global.exception.S3ExceptionHandlingUtil;
import bc1.gream.infra.s3.S3ImageService;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand All @@ -37,14 +39,28 @@ public SellBidResponseDto createSellBid(User seller, SellBidRequestDto requestDt

// 기프티콘 이미지 S3 저장
String url = s3ImageService.getUrlAfterUpload(requestDto.file());

// 기프티콘 생성, 저장
Gifticon gifticon = gifticonCommandService.saveGifticon(url, null);
Gifticon gifticon = S3ExceptionHandlingUtil.tryWithS3Cleanup(
() -> gifticonCommandService.saveGifticon(url, null),
s3ImageService,
url,
ResultCase.GIFTICON_SAVE_FAIL);
// 판매입찰 생성 및 저장
Sell savedSell = S3ExceptionHandlingUtil.tryWithS3Cleanup(
() -> saveSell(seller, requestDto, product, gifticon),
s3ImageService,
url,
ResultCase.SELL_BID_SAVE_FAIL);
// 매퍼로 변환
return SellMapper.INSTANCE.toSellBidResponseDto(savedSell);
}

@Transactional
Sell saveSell(User seller, SellBidRequestDto requestDto, Product product, Gifticon gifticon) {
// 마감기한 지정 : LocalTime.Max :: 23시 59분 59초
Integer period = Deadline.getPeriod(requestDto.period());
LocalDateTime deadlineAt = DeadlineCalculator.calculateDeadlineBy(LocalDate.now(), LocalTime.MAX,
period);

// 판매입찰 생성 및 저장
Sell sell = Sell.builder()
.price(requestDto.price())
Expand All @@ -53,13 +69,9 @@ public SellBidResponseDto createSellBid(User seller, SellBidRequestDto requestDt
.product(product)
.gifticon(gifticon)
.build();
Sell savedSell = sellRepository.save(sell);

// 매퍼로 변환
return SellMapper.INSTANCE.toSellBidResponseDto(savedSell);
return sellRepository.save(sell);
}


public SellCancelBidResponseDto sellCancelBid(User seller, Long sellId) {
Sell deletedSell = sellService.deleteSellByIdAndUser(sellId, seller);
gifticonCommandService.delete(deletedSell.getGifticon());
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/bc1/gream/domain/sell/provider/SellNowProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import bc1.gream.domain.sell.dto.request.SellNowRequestDto;
import bc1.gream.domain.sell.dto.response.SellNowResponseDto;
import bc1.gream.domain.user.entity.User;
import bc1.gream.global.common.ResultCase;
import bc1.gream.global.exception.S3ExceptionHandlingUtil;
import bc1.gream.infra.s3.S3ImageService;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -42,14 +44,21 @@ public SellNowResponseDto sellNowProduct(User user, SellNowRequestDto requestDto

// 기프티콘 이미지 S3 저장
String url = s3ImageService.getUrlAfterUpload(requestDto.file());

// 새로운 기프티콘 저장
gifticonCommandService.saveGifticon(url, order);
// 판매에 따른 사용자 포인트 충전
user.increasePoint(order.getExpectedPrice());

// 판매에 따른 사용자 포인트 충전
S3ExceptionHandlingUtil.tryWithS3Cleanup(
() -> user.increasePoint(order.getExpectedPrice()),
s3ImageService,
url,
ResultCase.USER_ADD_POINT_FAIL);
// 구매입찰 삭제
commandService.delete(buy);
S3ExceptionHandlingUtil.tryWithS3Cleanup(
() -> user.increasePoint(order.getExpectedPrice()),
s3ImageService,
url,
ResultCase.SELL_BID_DELETE_FAIL);

// 매퍼를 통해 변환
return OrderMapper.INSTANCE.toSellNowResponseDto(order);
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/bc1/gream/global/common/ResultCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ public enum ResultCase {
// 만료된 리프레쉬 토큰 401
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, 1006, "만료된 Refresh Token"),
NOT_ENOUGH_POINT(HttpStatus.FORBIDDEN, 1007, "유저의 포인트가 부족합니다"),
USER_ADD_POINT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 1008, "서버 내부 장애로 인해 유저의 포인트 충전 요청을 수행하지 못 하였습니다."),

// 상품 2000번대
// 검색 결과 없음 404
SEARCH_RESULT_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "검색 결과가 없습니다."),
// 존재하지 않는 상품 404
PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, 2001, "해당 상품은 존재하지 않습니다."),
GIFTICON_NOT_FOUND(HttpStatus.NOT_FOUND, 2002, "해당 id의 기프티콘은 존재하지 않습니다."),
GIFTICON_SAVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 2003, "서버 내부 장애로 인해 기프티콘 저장요청을 수행하지 못 하였습니다."),

// 구매 3000번대
// 구매 요청 대상 상품이 이미 판매되었음 409
Expand All @@ -45,9 +48,10 @@ public enum ResultCase {
// 판매 4000번대
// 판매 요청 대상 상품이 이미 구매되었음 409
SELL_PRODUCT_SOLD_OUT(HttpStatus.CONFLICT, 4000, "판매 요청 대상 상품이 이미 구매되었습니다."),
SELL_BID_DELETE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 4001, "서버 내부 장애로 인해 판매입찰 데이터 삭제요청을 수행하지 못 하였습니다."),
// 판매 요청에 대상 상품이 존재하지 않음 404
SELL_BID_PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, 4001, "해당 판매 입찰 건은 존재하지 않습니다."),
GIFTICON_NOT_FOUND(HttpStatus.NOT_FOUND, 4002, "해당 id의 기프티콘은 존재하지 않습니다."),
SELL_BID_SAVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 4002, "서버 내부 장애로 인해 판매입찰 생성요청을 수행하지 못 하였습니다."),
// 입찰 마감 기한 초과 409

// 글로벌 5000번대
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package bc1.gream.global.exception;

import bc1.gream.global.common.ResultCase;
import bc1.gream.infra.s3.S3ImageService;
import io.vavr.control.Try;
import org.springframework.dao.OptimisticLockingFailureException;

public class S3ExceptionHandlingUtil {

/**
* 타겟함수에 대해 실행, 성공 시 반환값을 반환하고, 실패 시 s3 이미지를 삭제한 뒤 예외처리합니다.
*
* @param targetFunction Function<T, R> 타겟함수
* @param s3ImageService s3 이미지 서비스
* @param imageUrl 이미지 URL
* @param resultCase 예외케이스
* @param <T> 타켓함수의 제너릭 클래스
* @return 타켓함수의 반환값
* @author 임지훈
*/
public static <T> T tryWithS3Cleanup(ThrowingFunction<T> targetFunction, S3ImageService s3ImageService, String imageUrl,
ResultCase resultCase) {
return Try.of(targetFunction::apply)
.onFailure(ex -> handleException(ex, s3ImageService, imageUrl))
.getOrElseThrow(() -> new GlobalException(resultCase));
}

/**
* 타겟함수에 대해 실행, 실패 시 s3 이미지를 삭제한 뒤 예외처리합니다.
*
* @param targetRunnable Runnable 타겟함수
* @param s3ImageService s3 이미지 서비스
* @param imageUrl 이미지 URL
* @param resultCase 예외케이스
*/
public static void tryWithS3Cleanup(ThrowingRunnable targetRunnable, S3ImageService s3ImageService, String imageUrl,
ResultCase resultCase) {
Try.run(targetRunnable::run)
.onFailure(ex -> handleException(ex, s3ImageService, imageUrl))
.getOrElseThrow(() -> new GlobalException(resultCase));
}

private static void handleException(Throwable ex, S3ImageService s3ImageService, String imageUrl) {
if (ex instanceof IllegalArgumentException || ex instanceof OptimisticLockingFailureException) {
// s3ImageService.delete(imageUrl);
}
}

@FunctionalInterface
public interface ThrowingFunction<T> {

T apply() throws Exception;
}


@FunctionalInterface
public interface ThrowingRunnable {

void run() throws Exception;
}
}