diff --git a/backend/build.gradle b/backend/build.gradle index 2eae6dabc..8a0ad47dd 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -29,6 +29,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.547' } tasks.named('test') { diff --git a/backend/src/main/java/com/funeat/common/OpenApiConfig.java b/backend/src/main/java/com/funeat/common/OpenApiConfig.java index ef3b45b2c..d89a133bc 100644 --- a/backend/src/main/java/com/funeat/common/OpenApiConfig.java +++ b/backend/src/main/java/com/funeat/common/OpenApiConfig.java @@ -15,6 +15,11 @@ @Tag(name = "01.Product", description = "상품 기능"), @Tag(name = "02.Category", description = "카테고리 기능"), @Tag(name = "03.Review", description = "리뷰 기능"), + @Tag(name = "04.Tag", description = "태그 기능"), + @Tag(name = "05.Member", description = "사용자 기능"), + @Tag(name = "06.Login", description = "로그인 기능"), + @Tag(name = "07.Recipe", description = "꿀조합 기능"), + @Tag(name = "08.S3", description = "S3 기능"), } ) @Configuration diff --git a/backend/src/main/java/com/funeat/common/controller/PreSingedApiController.java b/backend/src/main/java/com/funeat/common/controller/PreSingedApiController.java new file mode 100644 index 000000000..34ec9776d --- /dev/null +++ b/backend/src/main/java/com/funeat/common/controller/PreSingedApiController.java @@ -0,0 +1,28 @@ +package com.funeat.common.controller; + +import com.funeat.common.dto.S3UrlRequest; +import com.funeat.common.dto.S3UrlResponse; +import com.funeat.common.s3.S3UploadUrlGenerator; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PreSingedApiController implements PreSingedController { + + private final S3UploadUrlGenerator s3UploadUrlGenerator; + + public PreSingedApiController(final S3UploadUrlGenerator s3UploadUrlGenerator) { + this.s3UploadUrlGenerator = s3UploadUrlGenerator; + } + + @PostMapping("/api/s3/presigned") + public ResponseEntity getPreSingedUrl(@RequestBody final S3UrlRequest request) { + final S3UrlResponse preSignedUrl = s3UploadUrlGenerator.getPreSignedUrl(request.getFileName()); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(preSignedUrl); + } +} diff --git a/backend/src/main/java/com/funeat/common/controller/PreSingedController.java b/backend/src/main/java/com/funeat/common/controller/PreSingedController.java new file mode 100644 index 000000000..cc78d575a --- /dev/null +++ b/backend/src/main/java/com/funeat/common/controller/PreSingedController.java @@ -0,0 +1,20 @@ +package com.funeat.common.controller; + +import com.funeat.common.dto.S3UrlRequest; +import com.funeat.common.dto.S3UrlResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +public interface PreSingedController { + + @Operation(summary = "S3 업로드 URL 요청", description = "S3 업로드 URL 요청한다.") + @ApiResponse( + responseCode = "201", + description = "업로드 URL 요청 성공." + ) + @PostMapping + ResponseEntity getPreSingedUrl(@RequestBody final S3UrlRequest request); +} diff --git a/backend/src/main/java/com/funeat/common/dto/S3UrlRequest.java b/backend/src/main/java/com/funeat/common/dto/S3UrlRequest.java new file mode 100644 index 000000000..dc19c26ae --- /dev/null +++ b/backend/src/main/java/com/funeat/common/dto/S3UrlRequest.java @@ -0,0 +1,20 @@ +package com.funeat.common.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotBlank; + +public class S3UrlRequest { + + @NotBlank(message = "파일명을 확인해주세요") + private final String fileName; + + @JsonCreator + public S3UrlRequest(@JsonProperty("fileName") final String fileName) { + this.fileName = fileName; + } + + public String getFileName() { + return fileName; + } +} diff --git a/backend/src/main/java/com/funeat/common/dto/S3UrlResponse.java b/backend/src/main/java/com/funeat/common/dto/S3UrlResponse.java new file mode 100644 index 000000000..694149481 --- /dev/null +++ b/backend/src/main/java/com/funeat/common/dto/S3UrlResponse.java @@ -0,0 +1,14 @@ +package com.funeat.common.dto; + +public class S3UrlResponse { + + private final String preSingedUrl; + + public S3UrlResponse(final String preSingedUrl) { + this.preSingedUrl = preSingedUrl; + } + + public String getPreSingedUrl() { + return preSingedUrl; + } +} diff --git a/backend/src/main/java/com/funeat/common/s3/AwsConfig.java b/backend/src/main/java/com/funeat/common/s3/AwsConfig.java new file mode 100644 index 000000000..83a7e67d5 --- /dev/null +++ b/backend/src/main/java/com/funeat/common/s3/AwsConfig.java @@ -0,0 +1,28 @@ +package com.funeat.common.s3; + +import com.amazonaws.auth.InstanceProfileCredentialsProvider; +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 AwsConfig { + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public InstanceProfileCredentialsProvider awsCredentialsProvider() { + return InstanceProfileCredentialsProvider.getInstance(); + } + + @Bean + public AmazonS3 amazonS3Client() { + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(awsCredentialsProvider()) + .build(); + } +} diff --git a/backend/src/main/java/com/funeat/common/s3/S3UploadUrlGenerator.java b/backend/src/main/java/com/funeat/common/s3/S3UploadUrlGenerator.java new file mode 100644 index 000000000..6a55a395f --- /dev/null +++ b/backend/src/main/java/com/funeat/common/s3/S3UploadUrlGenerator.java @@ -0,0 +1,54 @@ +package com.funeat.common.s3; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.Headers; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.funeat.common.dto.S3UrlResponse; +import java.util.Date; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class S3UploadUrlGenerator { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.folder}") + private String folder; + + @Value("${cloud.aws.s3.expiration.time}") + private long expirationTime; + + private final AmazonS3 amazonS3; + + public S3UploadUrlGenerator(final AmazonS3 amazonS3) { + this.amazonS3 = amazonS3; + } + + public S3UrlResponse getPreSignedUrl(final String fileName) { + final GeneratePresignedUrlRequest generatePresignedUrlRequest = getPreSignedUrlRequest(bucket, fileName); + + return new S3UrlResponse(amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString()); + } + + private GeneratePresignedUrlRequest getPreSignedUrlRequest(final String bucket, final String fileName) { + final Date madeExpirationTime = getExpirationTime(); + final GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(bucket, + folder + fileName) + .withMethod(HttpMethod.PUT) + .withExpiration(madeExpirationTime); + + urlRequest.addRequestParameter(Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); + + return urlRequest; + } + + private Date getExpirationTime() { + final Date madeExpirationTime = new Date(); + madeExpirationTime.setTime(expirationTime); + return madeExpirationTime; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java b/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java index 8e74e5484..bed486ab9 100644 --- a/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java +++ b/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; -@Tag(name = "07. Recipe", description = "꿀조합 관련 API 입니다.") +@Tag(name = "07.Recipe", description = "꿀조합 관련 API 입니다.") public interface RecipeController { @Operation(summary = "꿀조합 추가", description = "꿀조합을 작성한다.") diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 026ad9f63..917190e0b 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -37,3 +37,13 @@ management: enabled: true server: port: { SERVER_PORT } + +cloud: + aws: + region: + static: { S3_REGION } + s3: + bucket: { S3_BUCKET } + folder: { S3_DEV_FOLDER } + expiration: + time: { S3_EXPIRATION } diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index d2c0a9790..e53f57b57 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -37,3 +37,12 @@ management: server: port: { SERVER_PORT } +cloud: + aws: + region: + static: { S3_REGION } + s3: + bucket: { S3_BUCKET } + folder: { S3_PROD_FOLDER } + expiration: + time: { S3_EXPIRATION } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 96e7e2c3c..9bfcfc96a 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -24,3 +24,13 @@ server: session: cookie: name: FUNEAT + +cloud: + aws: + region: + static: testRegion + s3: + bucket: testBucket + folder: testFolder + expiration: + time: 1111