Skip to content

Commit

Permalink
[MERGE] feat/#289 -> dev
Browse files Browse the repository at this point in the history
[FEAT/#289] 배너 이미지 pre-sigend url 조회 및 배너 생성 API 구현
  • Loading branch information
sung-silver authored Dec 23, 2024
2 parents af4084f + b41ad7a commit 0b1a0d5
Show file tree
Hide file tree
Showing 25 changed files with 417 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ public ResponseEntity<BaseResponse<?>> permissionException(PermissionException e
return ApiResponseUtil.failure(ex.getFailureCode());
}

@ExceptionHandler(BannerException.class)
public ResponseEntity<BaseResponse<?>> bannerException(BannerException ex) {
log.error(ex.getMessage());
return ApiResponseUtil.failure(ex.getFailureCode());
}

@ExceptionHandler(ExternalException.class)
public ResponseEntity<BaseResponse<?>> externalException(ExternalException ex) {
log.error(ex.getMessage());
return ApiResponseUtil.failure(ex.getFailureCode());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<BaseResponse<?>> validationException(MethodArgumentNotValidException ex) {
List<String> errorMessages = ex.getBindingResult().getAllErrors().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import org.sopt.makers.operation.dto.BaseResponse;

import org.sopt.makers.operation.web.banner.dto.request.*;
import org.springframework.http.ResponseEntity;

public interface BannerApi {
Expand All @@ -31,4 +32,42 @@ public interface BannerApi {
)
ResponseEntity<BaseResponse<?>> getBannerDetail(Long bannerId);

@Operation(
summary = "배너 이미지 PreSignedUrl 조회 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "PreSignedUrl 조회 성공"
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청"
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
}
)
ResponseEntity<BaseResponse<?>> getIssuedPreSignedUrlForPutImage(String contentName, String imageType, String imageExtension, String contentType);

@Operation(
summary = "배너 생성 API",
responses = {
@ApiResponse(
responseCode = "201",
description = "배너 생성 성공"
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청"
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
}
)
ResponseEntity<BaseResponse<?>> createBanner(BannerRequest.BannerCreate request);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

import org.sopt.makers.operation.dto.BaseResponse;
import org.sopt.makers.operation.util.ApiResponseUtil;
import org.sopt.makers.operation.web.banner.dto.request.BannerRequest;
import org.sopt.makers.operation.web.banner.service.BannerService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.bind.annotation.*;

import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_CREATE_BANNER;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_GET_BANNER_DETAIL;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_GET_BANNER_IMAGE_PRE_SIGNED_URL;

@RestController
@RequestMapping("/api/v1/banners")
Expand All @@ -28,4 +29,19 @@ public ResponseEntity<BaseResponse<?>> getBannerDetail(
val response = bannerService.getBannerDetail(bannerId);
return ApiResponseUtil.success(SUCCESS_GET_BANNER_DETAIL, response);
}

@Override
@GetMapping("/img/pre-signed")
public ResponseEntity<BaseResponse<?>> getIssuedPreSignedUrlForPutImage(@RequestParam("content-name") String contentName, @RequestParam("image-type") String imageType,
@RequestParam("image-extension") String imageExtension, @RequestParam("content-type") String contentType) {
val response = bannerService.getIssuedPreSignedUrlForPutImage(contentName, imageType, imageExtension, contentType);
return ApiResponseUtil.success(SUCCESS_GET_BANNER_IMAGE_PRE_SIGNED_URL, response);
}

@PostMapping
@Override
public ResponseEntity<BaseResponse<?>> createBanner(@RequestBody BannerRequest.BannerCreate request) {
val response = bannerService.createBanner(request);
return ApiResponseUtil.success(SUCCESS_CREATE_BANNER, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.sopt.makers.operation.web.banner.dto.request;

import static lombok.AccessLevel.PRIVATE;

import com.fasterxml.jackson.annotation.*;
import java.time.*;
import lombok.*;

@RequiredArgsConstructor(access = PRIVATE)
public class BannerRequest {

public record BannerCreate(
@JsonProperty("location") String bannerLocation,
@JsonProperty("content_type") String bannerType,
@JsonProperty("publisher") String publisher,
@JsonProperty("start_date") LocalDate startDate,
@JsonProperty("end_date") LocalDate endDate,
@JsonProperty("link") String link,
@JsonProperty("image_pc") String pcImage,
@JsonProperty("image_mobile") String mobileImage
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public record BannerDetail(
@JsonProperty("location") String bannerLocation,
@JsonProperty("content_type") String bannerType,
@JsonProperty("publisher") String publisher,
@JsonProperty("link") String link,
@JsonProperty("start_date") LocalDate startDate,
@JsonProperty("end_date") LocalDate endDate,
@JsonProperty("image_url_pc") String pcImageUrl,
Expand All @@ -32,11 +33,22 @@ public static BannerDetail fromEntity(Banner banner) {
.bannerLocation(banner.getLocation().getValue())
.bannerType(banner.getContentType().getValue())
.publisher(banner.getPublisher())
.link(banner.getLink())
.startDate(banner.getPeriod().getStartDate())
.endDate(banner.getPeriod().getEndDate())
.pcImageUrl(banner.getImage().getPcImageUrl())
.mobileImageUrl(banner.getImage().getMobileImageUrl())
.build();
}
}

@Builder(access = PRIVATE)
public record ImagePreSignedUrl(
@JsonProperty("presigned-url") String preSignedUrl,
@JsonProperty("filename") String fileName
) {
public static ImagePreSignedUrl of(String preSignedUrl, String filename) {
return new ImagePreSignedUrl(preSignedUrl, filename);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.sopt.makers.operation.web.banner.service;

import org.sopt.makers.operation.web.banner.dto.request.*;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;

public interface BannerService {

BannerResponse.BannerDetail getBannerDetail(final long bannerId);

BannerResponse.ImagePreSignedUrl getIssuedPreSignedUrlForPutImage(String contentName, String imageType, String imageExtension, String contentType);

BannerResponse.BannerDetail createBanner(BannerRequest.BannerCreate request);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package org.sopt.makers.operation.web.banner.service;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.banner.domain.Banner;
import org.sopt.makers.operation.banner.domain.*;
import org.sopt.makers.operation.banner.repository.BannerRepository;
import org.sopt.makers.operation.client.s3.S3Service;
import org.sopt.makers.operation.code.failure.BannerFailureCode;
import org.sopt.makers.operation.config.ValueConfig;
import org.sopt.makers.operation.exception.BannerException;
import org.sopt.makers.operation.web.banner.dto.request.BannerRequest.*;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BannerServiceImpl implements BannerService {

private final BannerRepository bannerRepository;
private final S3Service s3Service;
private final ValueConfig valueConfig;

@Override
public BannerResponse.BannerDetail getBannerDetail(final long bannerId) {
Expand All @@ -23,6 +33,63 @@ public BannerResponse.BannerDetail getBannerDetail(final long bannerId) {

private Banner getBannerById(final long id) {
return bannerRepository.findById(id)
.orElseThrow(() -> new BannerException(BannerFailureCode.NOT_FOUNT_BANNER));
.orElseThrow(() -> new BannerException(BannerFailureCode.NOT_FOUND_BANNER));
}

@Override
public BannerResponse.ImagePreSignedUrl getIssuedPreSignedUrlForPutImage(String contentName, String imageType, String imageExtension, String contentType) {
val type = ImageType.getByValue(imageType);
val extension = ImageExtension.getByValue(imageExtension);
val location = ContentType.getByValue(contentType).getLocation();
val fileName = getBannerImageName(location, contentName, type.getValue(), extension.getValue());
val putPreSignedUrl = s3Service.createPreSignedUrlForPutObject(valueConfig.getBannerBucket(), fileName);

return BannerResponse.ImagePreSignedUrl.of(putPreSignedUrl, fileName);
}

private String getBannerImageName(String location, String contentName, String imageType, String imageExtension) {
val today = LocalDate.now();
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
val formattedDate = today.format(formatter);

return location+formattedDate + "_" + contentName + "(" + imageType + ")." + imageExtension;
}

@Transactional
@Override
public BannerDetail createBanner(BannerCreate request) {
val period = getPublishPeriod(request.startDate(), request.endDate());
val image = getBannerImage(request.pcImage(), request.mobileImage());
val newBanner = Banner.builder()
.publisher(request.publisher())
.link(request.link())
.contentType(ContentType.getByValue(request.bannerType()))
.location(PublishLocation.getByValue(request.bannerLocation()))
.period(period)
.image(image)
.build();
val banner = saveBanner(newBanner);

return BannerResponse.BannerDetail.fromEntity(banner);
}

private PublishPeriod getPublishPeriod(LocalDate startDate, LocalDate endDate) {
return PublishPeriod.builder()
.startDate(startDate)
.endDate(endDate)
.build();
}

private BannerImage getBannerImage(String pcImage, String mobileImage) {
val pcImageUrl = s3Service.getUrl(valueConfig.getBannerBucket(), pcImage);
val mobileImageUrl = s3Service.getUrl(valueConfig.getBannerBucket(), mobileImage);
return BannerImage.builder()
.pcImageUrl(pcImageUrl)
.mobileImageUrl(mobileImageUrl)
.build();
}

private Banner saveBanner(Banner banner) {
return bannerRepository.save(banner);
}
}
6 changes: 5 additions & 1 deletion operation-api/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ cloud:
aws:
credentials:
accessKey: ${AWS_CREDENTIALS_ACCESS_KEY}
secretKey: ${AWS_CREDENTIALS_ACCESS_KEY}
secretKey: ${AWS_CREDENTIALS_SECRET_KEY}
eventBridge:
roleArn: ${AWS_CREDENTIALS_EVENTBRIDGE_ROLE_ARN}
s3:
banner:
name: ${BUCKET_FOR_BANNER}
region: ${AWS_REGION}

management:
endpoints:
Expand Down
4 changes: 4 additions & 0 deletions operation-api/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ cloud:
secretKey: test
eventBridge:
roleArn: test
s3:
banner:
name: banner
region: test

logging:
level:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class BannerApiControllerTest {
@BeforeEach
void setMockBanner() {
BannerResponse.BannerDetail mockBannerDetail = new BannerResponse.BannerDetail(
MOCK_BANNER_ID, "in_progress", "pg_community", "product", "publisher",
MOCK_BANNER_ID, "in_progress", "pg_community", "product", "publisher", "link",
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31), "image-url-pc", "image-url-mobile"
);

Expand Down Expand Up @@ -76,6 +76,7 @@ void getBannerDetail() throws Exception {
.andExpect(jsonPath("$.data.location").value(givenBannerDetail.bannerLocation()))
.andExpect(jsonPath("$.data.content_type").value(givenBannerDetail.bannerType()))
.andExpect(jsonPath("$.data.publisher").value(givenBannerDetail.publisher()))
.andExpect(jsonPath("$.data.link").value(givenBannerDetail.link()))
.andExpect(jsonPath("$.data.start_date").value(givenBannerDetail.startDate().toString()))
.andExpect(jsonPath("$.data.end_date").value(givenBannerDetail.endDate().toString()))
.andExpect(jsonPath("$.data.image_url_pc").value(givenBannerDetail.pcImageUrl()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;

@RequiredArgsConstructor
@Getter
public enum BannerFailureCode implements FailureCode {
INVALID_BANNER_PERIOD(BAD_REQUEST, "배너 게시 기간이 올바르지 않습니다."),
INVALID_IMAGE_EXTENSION(BAD_REQUEST, "지원하지 않는 배너 이미지 형식입니다."),
INVALID_IMAGE_TYPE(BAD_REQUEST, "지원하지 않는 이미지 타입입니다."),
NOT_FOUND_STATUS(NOT_FOUND, "존재하지 않는 게시 상태입니다."),
NOT_FOUND_LOCATION(NOT_FOUND, "존재하지 않는 게시 위치입니다."),
NOT_FOUND_CONTENT_TYPE(NOT_FOUND, "존재하지 않는 게시 유형입니다."),
NOT_FOUNT_BANNER(NOT_FOUND, "존재하지 않는 배너입니다."),
NOT_FOUND_BANNER(NOT_FOUND, "존재하지 않는 배너입니다."),
NOT_FOUND_BANNER_IMAGE(NOT_FOUND, "존재하지 않는 배너 이미지입니다.")
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.makers.operation.code.failure;

import static org.springframework.http.HttpStatus.*;

import lombok.*;
import org.springframework.http.*;

@RequiredArgsConstructor
@Getter
public enum ExternalFailureCode implements FailureCode {
NOT_FOUND_S3_RESOURCE(NOT_FOUND, "S3에서 객체를 찾지 못했습니다");

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
@RequiredArgsConstructor(access = PRIVATE)
public enum BannerSuccessCode implements SuccessCode {
SUCCESS_GET_BANNER_DETAIL(HttpStatus.OK, "배너 상세 정보 조회 성공"),
SUCCESS_GET_BANNER_IMAGE_PRE_SIGNED_URL(HttpStatus.OK, "이미지 업로드 pre signed url 조회에 성공했습니다"),
SUCCESS_CREATE_BANNER(HttpStatus.CREATED, "배너 생성에 성공했습니다")
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ public class ValueConfig {
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region}")
private String region;
@Value("${cloud.aws.eventBridge.roleArn}")
private String eventBridgeRoleArn;
@Value("${cloud.aws.s3.banner.name}")
private String bannerBucket;
@Value("${oauth.apple.key.id}")
private String appleKeyId;
@Value("${oauth.apple.key.path}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.makers.operation.exception;

import lombok.*;
import org.sopt.makers.operation.code.failure.*;

@Getter
public class ExternalException extends RuntimeException {
private final FailureCode failureCode;

public ExternalException(FailureCode failureCode) {
super("[ExternalException] : " + failureCode.getMessage());
this.failureCode = failureCode;
}

}
Loading

0 comments on commit 0b1a0d5

Please sign in to comment.