Skip to content

Commit

Permalink
Merge pull request #181 from Team-BC-1/fix/edit-sell-image-upload
Browse files Browse the repository at this point in the history
즉시 판매 및 판매 입찰에서 기프티콘 이미지를 받도록 수정
  • Loading branch information
yunjae62 authored Jan 20, 2024
2 parents 9e69d16 + 97e3c71 commit a4b6f3a
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 20 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-batch'
// Retry
implementation 'org.springframework.retry:spring-retry'
// AWS S3
implementation 'io.awspring.cloud:spring-cloud-starter-aws-secrets-manager-config:2.4.4'

/* DB */
// MySQL
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/bc1/gream/domain/gifticon/entity/Gifticon.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
Expand All @@ -26,6 +27,7 @@ public class Gifticon extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Lob
@Column(name = "gifticon_url")
private String gifticonUrl;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -39,7 +38,7 @@ public class SellBidController {
@Operation(summary = "판매입찰 체결 요청", description = "판매자의 상품에 대한 판매입찰 체결요청을 처리합니다.")
public RestResponse<SellBidResponseDto> createSellBid(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody SellBidRequestDto requestDto,
@Valid SellBidRequestDto requestDto,
@PathVariable Long productId
) {
Product product = productValidator.validateBy(productId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -35,7 +34,7 @@ public class SellNowController {
public RestResponse<SellNowResponseDto> sellNowProduct(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long productId,
@Valid @RequestBody SellNowRequestDto requestDto
@Valid SellNowRequestDto requestDto
) {
SellNowResponseDto responseDto = sellNowProvider.sellNowProduct(userDetails.getUser(), requestDto, productId);
return RestResponse.success(responseDto);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package bc1.gream.domain.sell.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import org.springframework.web.multipart.MultipartFile;

@Builder
public record SellBidRequestDto(
@NotNull(message = "가격 필드는 비울 수 없습니다.")
Long price,
Integer period,
@NotBlank(message = "기프티콘을 업로드해 주세요")
String gifticonUrl
@NotNull(message = "기프티콘을 업로드해 주세요")
MultipartFile file
) {

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package bc1.gream.domain.sell.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;

public record SellNowRequestDto(
@NotNull(message = "가격 필드는 비울 수 없습니다.")
Long price,
@NotBlank(message = "기프티콘을 업로드해 주세요")
String gifticonUrl
@NotNull(message = "기프티콘을 업로드해 주세요")
MultipartFile file
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,33 @@
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.infra.s3.S3ImageService;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class SellBidProvider {

private final SellRepository sellRepository;
private final GifticonCommandService gifticonCommandService;
private final SellService sellService;
private final S3ImageService s3ImageService;

@Transactional
public SellBidResponseDto createSellBid(User seller, SellBidRequestDto requestDto, Product product) {

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

// 기프티콘 생성, 저장
Gifticon gifticon = gifticonCommandService.saveGifticon(requestDto.gifticonUrl(), null);
Gifticon gifticon = gifticonCommandService.saveGifticon(url, null);
// 마감기한 지정 : LocalTime.Max :: 23시 59분 59초
Integer period = Deadline.getPeriod(requestDto.period());
LocalDateTime deadlineAt = DeadlineCalculator.calculateDeadlineBy(LocalDate.now(), LocalTime.MAX,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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.infra.s3.S3ImageService;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -27,18 +28,23 @@ public class SellNowProvider {
private final CouponQueryService couponQueryService;
private final OrderCommandService orderCommandService;
private final GifticonCommandService gifticonCommandService;
private final S3ImageService s3ImageService;

@Transactional
public SellNowResponseDto sellNowProduct(User user, SellNowRequestDto requestDto, Long productId) {

// 해당상품과 가격에 대한 구매입찰
Buy buy = buyQueryService.getRecentBuyBidOf(productId, requestDto.price());
// 쿠폰 조회
Coupon coupon = getCouponFrom(buy);
// 새로운 주문
Order order = saveOrder(buy, user, coupon);

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

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

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 @@ -67,7 +67,11 @@ public enum ResultCase {
COUPON_ALREADY_IN_USED(HttpStatus.NOT_FOUND, 6002, "해당 쿠폰은 이미 사용중입니다."),
COUPON_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, 6003, "해당 쿠폰 종류는 존재하지 않는 종류입니다."),
COUPON_TYPE_INVALID_RATE(HttpStatus.CONFLICT, 6004, "올바른 형식의 고정할인가를 입력해주세요."),
COUPON_TYPE_INVALID_FIXED(HttpStatus.CONFLICT, 6005, "올바른 형식의 할인율을 입력해주세요.");
COUPON_TYPE_INVALID_FIXED(HttpStatus.CONFLICT, 6005, "올바른 형식의 할인율을 입력해주세요."),

// S3 7000번대
IMAGE_UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 7000, "이미지 업로드에 실패했습니다.");

private final HttpStatus httpStatus;
private final Integer code;
private final String message;
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/bc1/gream/infra/s3/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package bc1.gream.infra.s3;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3 amazonS3() {
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.build();
}
}
64 changes: 64 additions & 0 deletions src/main/java/bc1/gream/infra/s3/S3ImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package bc1.gream.infra.s3;

import bc1.gream.global.common.ResultCase;
import bc1.gream.global.exception.GlobalException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import java.io.IOException;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Slf4j(topic = "s3 image upload")
@Service
@RequiredArgsConstructor
public class S3ImageService {

private static final String gifticonDirectoryName = "gifticon/";
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;

@Transactional
public String getUrlAfterUpload(MultipartFile image) {

String originalName = image.getOriginalFilename();
String randomImageName = getRandomImageName(originalName);
log.info("s3 upload name : {}", randomImageName);

try {
PutObjectRequest request = new PutObjectRequest(
bucketName,
randomImageName,
image.getInputStream(),
createObjectMetadata(originalName)
)
.withCannedAcl(CannedAccessControlList.PublicRead);
amazonS3.putObject(request);
} catch (IOException e) {
throw new GlobalException(ResultCase.IMAGE_UPLOAD_FAIL);
}

String url = amazonS3.getUrl(bucketName, randomImageName).toString();
log.info("s3 url : {}", url);
return url;
}

private String getRandomImageName(String originalName) {
String random = UUID.randomUUID().toString();
return gifticonDirectoryName + random + '-' + originalName;
}

private ObjectMetadata createObjectMetadata(String originalName) {
ObjectMetadata metadata = new ObjectMetadata();
String ext = originalName.substring(originalName.lastIndexOf("."));
metadata.setContentType("image/" + ext);
return metadata;
}
}
12 changes: 11 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ spring:
# JPA
jpa:
hibernate:
ddl-auto: update
ddl-auto: none
properties:
hibernate:
auto_quote_keyword: true # 예약어 사용가능
Expand All @@ -35,3 +35,13 @@ server:
access-log: # ACESS 에 대한 로깅
enabled: true
pattern: "%{yyyy-MM-dd HH:mm:ss}t\\t%s\\t%r\\t%{User-Agent}i\\t%{Referer}i\\t%a\\t%b"
# s3
cloud:
aws:
s3:
bucket: ${BUCKET_NAME}
stack.auto: false
region.static: ${REGION}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_ACCESS_KEY}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import bc1.gream.domain.sell.provider.SellBidProvider;
import bc1.gream.domain.sell.repository.SellRepository;
import bc1.gream.domain.user.entity.User;
import bc1.gream.infra.s3.S3ImageService;
import bc1.gream.test.GifticonTest;
import java.io.IOException;
import org.junit.jupiter.api.Test;
Expand All @@ -34,6 +35,8 @@ class SellBidProviderTest implements GifticonTest {
GifticonCommandService gifticonCommandService;
@Mock
SellService sellService;
@Mock
S3ImageService s3ImageService;

@InjectMocks
SellBidProvider sellBidProvider;
Expand All @@ -47,10 +50,11 @@ void sellBidProductTest() throws IOException {

SellBidRequestDto requestDto = SellBidRequestDto.builder()
.price(TEST_SELL_PRICE)
.gifticonUrl(fileResource.getURL().getPath())
.file(TEST_GIFTICON_FILE)
.build();

given(gifticonCommandService.saveGifticon(requestDto.gifticonUrl(), null)).willReturn(TEST_GIFTICON);
given(s3ImageService.getUrlAfterUpload(requestDto.file())).willReturn(TEST_S3_IMAGE_URL);
given(gifticonCommandService.saveGifticon(TEST_S3_IMAGE_URL, null)).willReturn(TEST_GIFTICON);
given(sellRepository.save(any(Sell.class))).willReturn(TEST_SELL);

// when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private void insertBuyByCount(int buyCount) {
@Test
public void 즉시판매조회() {
// GIVEN
SellNowRequestDto requestDto = new SellNowRequestDto(TEST_BUY_PRICE, TEST_GIFTICON_URL);
SellNowRequestDto requestDto = new SellNowRequestDto(TEST_BUY_PRICE, TEST_GIFTICON_FILE);

// WHEN
SellNowResponseDto responseDto = sellNowProvider.sellNowProduct(savedSeller, requestDto, savedIcedAmericano.getId());
Expand All @@ -65,7 +65,7 @@ private void insertBuyByCount(int buyCount) {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
SellNowRequestDto requestDto = new SellNowRequestDto(TEST_BUY_PRICE, TEST_GIFTICON_URL);
SellNowRequestDto requestDto = new SellNowRequestDto(TEST_BUY_PRICE, TEST_GIFTICON_FILE);

// WHEN
List<SellNowResponseDto> sellNowResponseDtos = new ArrayList<>();
Expand Down
4 changes: 4 additions & 0 deletions src/test/java/bc1/gream/test/GifticonTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package bc1.gream.test;

import bc1.gream.domain.gifticon.entity.Gifticon;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

public interface GifticonTest extends OrderTest {

Long TEST_GIFTICON_ID = 1L;

String TEST_GIFTICON_URL = "images/images.png";
MultipartFile TEST_GIFTICON_FILE = new MockMultipartFile("file", "image.png", "image/png", "content".getBytes());
String TEST_S3_IMAGE_URL = "https://cataas.com/cat";

Gifticon TEST_GIFTICON_END = Gifticon.builder()
.gifticonUrl(TEST_GIFTICON_URL)
Expand Down
4 changes: 2 additions & 2 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ spring:
# JPA
jpa:
database: h2
generate-ddl: off
generate-ddl: on
defer-datasource-initialization: true
properties:
hibernate:
ddl-auto: none
ddl-auto: create
auto_quote_keyword: true # 예약어 사용가능
globally_quoted_identifiers: true # 예약어 사용가능
show_sql: true # sql 로깅
Expand Down

0 comments on commit a4b6f3a

Please sign in to comment.