Skip to content

Commit

Permalink
[feat] S3 적용
Browse files Browse the repository at this point in the history
[feat] S3 적용
  • Loading branch information
kmjenny authored Jun 7, 2024
2 parents 6365c77 + 80132fc commit 56f7a26
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 19 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ dependencies {

// CI/CD health check
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// Multipart file
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")
}

tasks.named('test') {
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/com/telepigeon/server/config/AwsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.telepigeon.server.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class AwsConfig {

private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId";
private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey";

private final String accessKey;
private final String secretKey;
private final String regionString;

public AwsConfig(@Value("${aws-property.access-key}") final String accessKey,
@Value("${aws-property.secret-key}") final String secretKey,
@Value("${aws-property.aws-region}") final String regionString) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.regionString = regionString;
}

@Bean
public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() {
System.setProperty(AWS_ACCESS_KEY_ID, accessKey);
System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey);
return SystemPropertyCredentialsProvider.create();
}

@Bean
public Region getRegion() {
return Region.of(regionString);
}

@Bean
public S3Client getS3Client() {
return S3Client.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.net.URI;
import java.time.LocalDate;
import java.time.YearMonth;
Expand All @@ -29,8 +30,8 @@ public ResponseEntity<Void> postAnswer(
@UserId final Long userId,
@PathVariable final Long roomId,
@PathVariable final Long questionId,
@RequestBody @Valid final AnswerCreateDto answerCreateDto
) {
@ModelAttribute @Valid final AnswerCreateDto answerCreateDto
) throws IOException {
return ResponseEntity.created(
URI.create(
"/answers/" + answerService.create(
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/com/telepigeon/server/domain/Answer.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ public class Answer {
private Profile profile;

public static Answer create(
AnswerCreateDto answerCreateDto,
String content,
String image,
Double emotion,
Question question,
Profile profile
){
return new Answer(
answerCreateDto.content(),
answerCreateDto.image(),
content,
image,
emotion,
question,
profile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

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

public record AnswerCreateDto(
@NotBlank
String content,
@Nullable
String image
MultipartFile image
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import com.telepigeon.server.dto.naverCloud.ConfidenceDto;
import com.telepigeon.server.dto.room.response.RoomStateDto;
import com.telepigeon.server.dto.type.FcmContent;
import com.telepigeon.server.service.external.S3Service;
import com.telepigeon.server.service.fcm.FcmService;
import com.telepigeon.server.service.naverCloud.NaverCloudService;
import com.telepigeon.server.service.external.NaverCloudService;
import com.telepigeon.server.service.user.UserRetriever;
import com.telepigeon.server.service.hurry.HurryRetriever;
import com.telepigeon.server.service.profile.ProfileRetriever;
Expand All @@ -23,6 +24,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Collections;
Expand All @@ -43,14 +45,17 @@ public class AnswerService {
private final HurryRetriever hurryRetriever;
private final NaverCloudService naverCloudService;
private final FcmService fcmService;
private final S3Service s3Service;

private static String ANSWER_S3_UPLOAD_FOLDER = "/answer";

@Transactional
public Answer create(
final Long userId,
final Long roomId,
final Long questionId,
final AnswerCreateDto answerCreateDto
){
) throws IOException {
User user = userRetriever.findById(userId);
Room room = roomRetriever.findById(roomId);
Profile profile = profileRetriever.findByUserAndRoom(user, room);
Expand All @@ -60,8 +65,11 @@ public Answer create(
);
Double emotion = (confidence.positive() - confidence.negative()) * 0.01;
Answer answer = answerSaver.create(
Answer.create(answerCreateDto, emotion, question, profile)
Answer.create(answerCreateDto.content(),
s3Service.uploadImage(ANSWER_S3_UPLOAD_FOLDER, answerCreateDto.image()),
emotion, question, profile)
);

profile.updateEmotion(
CalculateEmotion(
profile.getEmotion(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.telepigeon.server.service.naverCloud;
package com.telepigeon.server.service.external;

import com.telepigeon.server.dto.naverCloud.request.ConfidenceCreateDto;
import com.telepigeon.server.dto.naverCloud.ConfidenceDto;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.telepigeon.server.service.external;

import com.telepigeon.server.config.AwsConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

@Component
public class S3Service {

private final String bucketName;
private final AwsConfig awsConfig;
private static final List<String> IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp");

public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AwsConfig awsConfig) {
this.bucketName = bucketName;
this.awsConfig = awsConfig;
}

public String uploadImage(String directoryPath, MultipartFile image) throws IOException {
final String key = directoryPath + generateImageFileName();
final S3Client s3Client = awsConfig.getS3Client();

validateExtension(image);
validateFileSize(image);

PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(image.getContentType())
.contentDisposition("inline")
.build();

RequestBody requestBody = RequestBody.fromBytes(image.getBytes());
s3Client.putObject(request, requestBody);
return key;
}

public void deleteImage(String key) throws IOException {
final S3Client s3Client = awsConfig.getS3Client();

s3Client.deleteObject((DeleteObjectRequest.Builder builder) ->
builder.bucket(bucketName)
.key(key)
.build());
}

private String generateImageFileName() {
return UUID.randomUUID() + ".jpg";
}

private void validateExtension(MultipartFile image) {
String contentType = image.getContentType();
if(!IMAGE_EXTENSIONS.contains(contentType)) {
throw new RuntimeException("이미지 확장자는 jpg, png, webp만 가능합니다.");
}
}

private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L;

private void validateFileSize(MultipartFile image) {
if (image.getSize() > MAX_FILE_SIZE) {
throw new RuntimeException("이미지 사이즈는 5MB를 넘을 수 없습니다.");
}
}
}
8 changes: 7 additions & 1 deletion src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@ openai:
model: gpt-3.5-turbo
api:
key: ${openai_api_key}
url: ${openai_api_url}
url: ${openai_api_url}

aws-property:
s3-bucket-name: ${bucket_name}
access-key: ${access_key}
secret-key: ${secret_key}
aws-region: ap-northeast-2
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.telepigeon.server.answer;
package com.telepigeon.server.answerTest;

import com.telepigeon.server.domain.Answer;
import com.telepigeon.server.dto.answer.request.AnswerCreateDto;
Expand All @@ -10,16 +10,16 @@ public class AnswerDomainTest {
@Test
@DisplayName("Answer 객체 생성")
void createAnswerTest(){
AnswerCreateDto answerCreateDto = new AnswerCreateDto("content", "image");
Answer answer = Answer.create(answerCreateDto, 0.0, null, null);
AnswerCreateDto answerCreateDto = new AnswerCreateDto("content", null);
Answer answer = Answer.create(answerCreateDto.content(), null, null, null, null);
Assertions.assertNotNull(answer);
}

@Test
@DisplayName("Answer 객체 생성 확인")
void checkCreateAnswerTest(){
AnswerCreateDto answerCreateDto = new AnswerCreateDto("content", "image");
Answer answer = Answer.create(answerCreateDto, 0.0, null, null);
AnswerCreateDto answerCreateDto = new AnswerCreateDto("content", null);
Answer answer = Answer.create(answerCreateDto.content(), null, null, null, null);
Assertions.assertEquals(answer.getContent(), "content");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.telepigeon.server.auth;
package com.telepigeon.server.authTest;

import com.telepigeon.server.domain.User;
import com.telepigeon.server.dto.auth.response.JwtTokensDto;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.telepigeon.server.question;
package com.telepigeon.server.questionTest;

import com.telepigeon.server.domain.Question;
import org.junit.jupiter.api.Assertions;
Expand Down
8 changes: 7 additions & 1 deletion src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ spring:
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
dialect: org.hibernate.dialect.MySQL8Dialect

aws-property:
s3-bucket-name: ${bucket_name}
access-key: ${access_key}
secret-key: ${secret_key}
aws-region: ap-northeast-2

0 comments on commit 56f7a26

Please sign in to comment.