Skip to content

Commit

Permalink
[BSVR-91] 경기장 등록 API & 이미지 업로드 컴포넌트 (#33)
Browse files Browse the repository at this point in the history
* feat: 경기장 생성 컨트롤러 추가

* fix: response dto mapping 에러 수정

* feat: bucketName config 상수로 변경

* refactor: 가독성 개선

* test: FileNameGeneratorTest 수정
  • Loading branch information
EunjiShin authored Jul 16, 2024
1 parent 71eefb0 commit cc74697
Show file tree
Hide file tree
Showing 20 changed files with 272 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,4 @@ public MediaUrlResponse createReviewImageUploadUrl(
String presignedUrl = createPresignedUrlPort.forReview(memberId, command);
return new MediaUrlResponse(presignedUrl);
}

@ResponseStatus(HttpStatus.CREATED)
@PostMapping(value = "/stadiums/images")
@Operation(summary = "공연장 이미지 업로드 url을 생성합니다.")
public MediaUrlResponse createStadiumSeatUploadUrl(
@RequestBody @Valid CreatePresignedUrlRequest request) {
PresignedUrlRequest command =
new PresignedUrlRequest(request.fileExtension(), request.property());
String presignedUrl = createPresignedUrlPort.forStadiumSeat(command);
return new MediaUrlResponse(presignedUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.depromeet.spot.application.stadium;

import org.depromeet.spot.application.stadium.dto.response.StadiumResponse;
import org.depromeet.spot.domain.stadium.Stadium;
import org.depromeet.spot.usecase.port.in.stadium.CreateStadiumUsecase;
import org.depromeet.spot.usecase.port.in.stadium.CreateStadiumUsecase.CreateStadiumReq;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@Tag(name = "경기장")
@RequiredArgsConstructor
@RequestMapping("/api/v1/stadiums")
public class CreateStadiumController {

private final CreateStadiumUsecase createStadiumUsecase;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "신규 야구 경기장을 등록한다.")
public StadiumResponse create(
@RequestParam("name") String name,
@RequestParam("mainImage") MultipartFile mainImage,
@RequestParam("seatingChartImage") MultipartFile seatingChartImage,
@RequestParam("labeledSeatingChartImage") MultipartFile labeledSeatingChartImage,
@RequestParam("isActive") boolean isActive) {
CreateStadiumReq req =
CreateStadiumReq.builder()
.name(name)
.mainImage(mainImage)
.seatingChartImage(seatingChartImage)
.labeledSeatingChartImage(labeledSeatingChartImage)
.isActive(isActive)
.build();
Stadium stadium = createStadiumUsecase.create(req);
return StadiumResponse.from(stadium);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.depromeet.spot.application.stadium.dto.response;

import org.depromeet.spot.domain.stadium.Stadium;

import lombok.Builder;

@Builder
public record StadiumResponse(
Long id,
String name,
String mainImage,
String seatingChartImage,
String labeledSeatingChartImage,
boolean isActive) {

public static StadiumResponse from(Stadium stadium) {
return StadiumResponse.builder()
.id(stadium.getId())
.name(stadium.getName())
.mainImage(stadium.getMainImage())
.seatingChartImage(stadium.getSeatingChartImage())
.labeledSeatingChartImage(stadium.getLabeledSeatingChartImage())
.isActive(stadium.isActive())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum MediaErrorCode implements ErrorCode {
INVALID_STADIUM_MEDIA(HttpStatus.BAD_REQUEST, "ME002", "경기장과 관련된 미디어 파일이 아닙니다."),
INVALID_REVIEW_MEDIA(HttpStatus.BAD_REQUEST, "ME003", "리뷰와 관련된 미디어 파일이 아닙니다."),
INVALID_MEDIA(HttpStatus.INTERNAL_SERVER_ERROR, "ME004", "잘못된 미디어 형식입니다."),
UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "ME005", "파일 업로드에 실패했습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ public InvalidMediaException() {
super(MediaErrorCode.INVALID_MEDIA);
}
}

public static class UploadFailException extends MediaException {
public UploadFailException() {
super(MediaErrorCode.UPLOAD_FAIL);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
package org.depromeet.spot.domain.media;

import lombok.Getter;

@Getter
public enum MediaProperty {
REVIEW,
STADIUM,
REVIEW("review-images"),
STADIUM("stadium-images"),
STADIUM_SEAT("stadium-seat-charts"),
STADIUM_SEAT_LABEL("stadium-seat-label-charts"),
TEAM_LOGO("team-logos"),
;

private final String folderName;

MediaProperty(final String folderName) {
this.folderName = folderName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Repository
@RequiredArgsConstructor
public class StadiumRepositoryImpl implements StadiumRepository {
Expand All @@ -32,8 +34,8 @@ public List<Stadium> findAll() {

@Override
public Stadium save(Stadium stadium) {
// TODO: test를 위해 추가 -> 구장 저장 API 티켓때 구현 예정
return null;
StadiumEntity entity = stadiumJpaRepository.save(StadiumEntity.from(stadium));
return entity.toDomain();
}

@Override
Expand Down
1 change: 1 addition & 0 deletions infrastructure/ncp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dependencies {

// spring
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")

// ncp
implementation("org.springframework.cloud:spring-cloud-starter-aws:_") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ObjectStorageConfig {
private final ObjectStorageProperties objectStorageProperties;
private static final String ENDPOINT = "https://kr.object.ncloudstorage.com";
private static final String REGION = "kr-standard";
public static final String BUCKET_NAME = "spot-image-bucket";

@Bean
public AmazonS3 getAmazonS3() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.depromeet.spot.ncp.objectstorage;

import java.io.IOException;
import java.io.InputStream;

import org.depromeet.spot.common.exception.media.MediaException.InvalidExtensionException;
import org.depromeet.spot.common.exception.media.MediaException.UploadFailException;
import org.depromeet.spot.domain.media.MediaProperty;
import org.depromeet.spot.domain.media.extension.ImageExtension;
import org.depromeet.spot.domain.media.extension.StadiumSeatMediaExtension;
import org.depromeet.spot.ncp.config.ObjectStorageConfig;
import org.depromeet.spot.usecase.port.out.media.ImageUploadPort;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class ImageUploader implements ImageUploadPort {

private final AmazonS3 amazonS3;

@Override
public String upload(String targetName, MultipartFile file, MediaProperty property) {
if (file == null || file.isEmpty()) return null;

final String fileExtension = StringUtils.getFilenameExtension(file.getOriginalFilename());
checkValidExtension(fileExtension, property);

final String bucketName = ObjectStorageConfig.BUCKET_NAME;
final String fileName = createFileName(targetName, property.getFolderName(), fileExtension);
ObjectMetadata objectMetadata = createObjectMetadata(file);

try (InputStream inputStream = file.getInputStream()) {
uploadToS3(bucketName, fileName, inputStream, objectMetadata);
} catch (IOException e) {
throw new UploadFailException();
}

return amazonS3.getUrl(bucketName, fileName).toString();
}

private ObjectMetadata createObjectMetadata(MultipartFile file) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
return metadata;
}

private void uploadToS3(
String bucketName, String fileName, InputStream inputStream, ObjectMetadata metadata) {
amazonS3.putObject(
new PutObjectRequest(bucketName, fileName, inputStream, metadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
}

private void checkValidExtension(final String fileExtension, MediaProperty property) {
if (property == MediaProperty.STADIUM_SEAT
|| property == MediaProperty.STADIUM_SEAT_LABEL) {
checkValidSeatExtension(fileExtension);
} else {
checkValidImageExtension(fileExtension);
}
}

private void checkValidSeatExtension(final String fileExtension) {
if (!StadiumSeatMediaExtension.isValid(fileExtension)) {
throw new InvalidExtensionException(fileExtension);
}
}

private void checkValidImageExtension(final String fileExtension) {
if (!ImageExtension.isValid(fileExtension)) {
throw new InvalidExtensionException(fileExtension);
}
}

private String createFileName(
final String targetName, final String folderName, final String fileExtension) {
return folderName + "/" + targetName + "." + fileExtension;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@

import org.depromeet.spot.common.exception.media.MediaException.InvalidExtensionException;
import org.depromeet.spot.common.exception.media.MediaException.InvalidReviewMediaException;
import org.depromeet.spot.common.exception.media.MediaException.InvalidStadiumMediaException;
import org.depromeet.spot.domain.media.MediaProperty;
import org.depromeet.spot.domain.media.extension.ImageExtension;
import org.depromeet.spot.domain.media.extension.StadiumSeatMediaExtension;
import org.depromeet.spot.ncp.property.ReviewStorageProperties;
import org.depromeet.spot.ncp.property.StadiumStorageProperties;
import org.depromeet.spot.ncp.config.ObjectStorageConfig;
import org.depromeet.spot.usecase.port.out.media.CreatePresignedUrlPort;
import org.springframework.stereotype.Service;

Expand All @@ -22,16 +19,16 @@

import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@Builder
@RequiredArgsConstructor
public class PresignedUrlGenerator implements CreatePresignedUrlPort {

private final AmazonS3 amazonS3;
private final FileNameGenerator fileNameGenerator;
private final ReviewStorageProperties reviewStorageProperties;
private final StadiumStorageProperties stadiumStorageProperties;

private static final long EXPIRE_MS = 1000 * 60 * 5L;

Expand All @@ -40,10 +37,10 @@ public String forReview(final Long userId, PresignedUrlRequest request) {
isValidReviewMedia(request.getProperty(), request.getFileExtension());

final ImageExtension fileExtension = ImageExtension.from(request.getFileExtension());
final String folderName = reviewStorageProperties.folderName();
final String folderName = request.getProperty().getFolderName();
final String fileName =
fileNameGenerator.createReviewFileName(userId, fileExtension, folderName);
final URL url = createPresignedUrl(reviewStorageProperties.bucketName(), fileName);
final URL url = createPresignedUrl(fileName);

return url.toString();
}
Expand All @@ -59,38 +56,14 @@ private void isValidReviewMedia(final MediaProperty property, final String fileE
}
}

@Override
public String forStadiumSeat(PresignedUrlRequest request) {
isValidStadiumMedia(request.getProperty(), request.getFileExtension());

final StadiumSeatMediaExtension fileExtension =
StadiumSeatMediaExtension.from(request.getFileExtension());
final String folderName = stadiumStorageProperties.folderName();
final String fileName = fileNameGenerator.createStadiumFileName(fileExtension, folderName);
final URL url = createPresignedUrl(stadiumStorageProperties.bucketName(), fileName);

return url.toString();
}

private void isValidStadiumMedia(final MediaProperty property, final String fileExtension) {
if (property != MediaProperty.STADIUM) {
throw new InvalidStadiumMediaException();
}

if (!StadiumSeatMediaExtension.isValid(fileExtension)) {
throw new InvalidExtensionException(fileExtension);
}
}

private URL createPresignedUrl(final String bucketName, final String fileName) {
return amazonS3.generatePresignedUrl(
createGeneratePreSignedUrlRequest(bucketName, fileName));
private URL createPresignedUrl(final String fileName) {
return amazonS3.generatePresignedUrl(createGeneratePreSignedUrlRequest(fileName));
}

private GeneratePresignedUrlRequest createGeneratePreSignedUrlRequest(
final String bucket, final String fileName) {
private GeneratePresignedUrlRequest createGeneratePreSignedUrlRequest(final String fileName) {
final String bucketName = ObjectStorageConfig.BUCKET_NAME;
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
new GeneratePresignedUrlRequest(bucketName, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(createPreSignedUrlExpiration());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@

import org.springframework.boot.context.properties.ConfigurationProperties;

// FIXME: ncp 세팅 완료 후, applicaiton.yml 참고해서 prefix 추가
@ConfigurationProperties(prefix = "ncp.object-storage")
public record ObjectStorageProperties(
String accessKey, String secretKey, String region, String endPoint) {}
public record ObjectStorageProperties(String accessKey, String secretKey) {}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
ncp:
object-storage:
accessKey: ${NCP_OBJECT_STORAGE_ACCESS_KEY}
secretKey: ${NCP_OBJECT_STORAGE_SECRET_KEY}
review-storage:
bucketName: "spot-image-bucket"
folderName: "review-images"
stadium-storage:
bucketName: "spot-image-bucket"
folderName: "stadium-images"
secretKey: ${NCP_OBJECT_STORAGE_SECRET_KEY}
Loading

0 comments on commit cc74697

Please sign in to comment.