diff --git a/build.gradle b/build.gradle index 91cc2e77d..014a60951 100644 --- a/build.gradle +++ b/build.gradle @@ -65,8 +65,9 @@ dependencies { testImplementation 'org.awaitility:awaitility:4.2.0' // Etc + implementation platform('software.amazon.awssdk:bom:2.41.4') + implementation 'software.amazon.awssdk:s3' implementation 'org.hibernate.validator:hibernate-validator' - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782' implementation 'org.springframework.boot:spring-boot-starter-websocket' } diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 57f8cad65..bdb2d21b7 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -27,6 +27,7 @@ import com.example.solidconnection.mentor.repository.MentorRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -39,6 +40,7 @@ import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; @Service public class ChatService { @@ -240,16 +242,19 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)); - ChatMessage chatMessage = new ChatMessage( - "", - senderId, - chatRoom - ); + ChatMessage chatMessage = new ChatMessage("", senderId, chatRoom); + + // 이미지 판별을 위한 확장자 리스트 + List imageExtensions = Arrays.asList("jpg", "jpeg", "png", "webp"); for (String imageUrl : chatImageSendRequest.imageUrls()) { - String thumbnailUrl = generateThumbnailUrl(imageUrl); + String extension = StringUtils.getFilenameExtension(imageUrl); - ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null); + boolean isImage = extension != null && imageExtensions.contains(extension.toLowerCase()); + + String thumbnailUrl = isImage ? generateThumbnailUrl(imageUrl) : null; + + ChatAttachment attachment = new ChatAttachment(isImage, imageUrl, thumbnailUrl, null); chatMessage.addAttachment(attachment); } @@ -268,11 +273,9 @@ private String generateThumbnailUrl(String originalUrl) { String thumbnailFileName = nameWithoutExt + "_thumb" + extension; - String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/") + return originalUrl.replace("chat/files/", "chat/thumbnails/") .replace(fileName, thumbnailFileName); - return thumbnailUrl; - } catch (Exception e) { return originalUrl; } diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java index 4b3b8d15a..8b85aa651 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java @@ -18,7 +18,7 @@ import com.example.solidconnection.community.post.dto.PostUpdateRequest; import com.example.solidconnection.community.post.dto.PostUpdateResponse; import com.example.solidconnection.community.post.repository.PostRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -88,7 +88,7 @@ private void savePostImages(List imageFile, Post post) { if (imageFile.isEmpty()) { return; } - List uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, ImgType.COMMUNITY); + List uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, UploadType.COMMUNITY); for (UploadedFileUrlResponse uploadedFileUrlResponse : uploadedFileUrlResponseList) { PostImage postImage = new PostImage(uploadedFileUrlResponse.fileUrl()); postImage.setPost(post); diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java index e4e187808..6bd8e564f 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -5,7 +5,7 @@ import com.example.solidconnection.mentor.domain.MentorApplicationStatus; import com.example.solidconnection.mentor.dto.MentorApplicationRequest; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -45,7 +45,7 @@ public void submitMentorApplication( .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); Term term = termRepository.findByName(mentorApplicationRequest.term()) .orElseThrow(() -> new CustomException(TERM_NOT_FOUND)); - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadType.MENTOR_PROOF); MentorApplication mentorApplication = new MentorApplication( siteUser.getId(), mentorApplicationRequest.country(), diff --git a/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java b/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java index ca1b262fe..8fe390aaf 100644 --- a/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java +++ b/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java @@ -11,7 +11,7 @@ import com.example.solidconnection.news.dto.NewsCreateRequest; import com.example.solidconnection.news.dto.NewsUpdateRequest; import com.example.solidconnection.news.repository.NewsRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.Role; @@ -41,7 +41,7 @@ public NewsCommandResponse createNews(long siteUserId, NewsCreateRequest newsCre private String getImageUrl(MultipartFile imageFile) { if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadType.NEWS); return uploadedFile.fileUrl(); } return newsProperties.defaultThumbnailUrl(); @@ -73,7 +73,7 @@ private void updateThumbnail(News news, MultipartFile imageFile, Boolean resetTo deleteCustomImage(news.getThumbnailUrl()); news.updateThumbnailUrl(newsProperties.defaultThumbnailUrl()); } else if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadType.NEWS); deleteCustomImage(news.getThumbnailUrl()); news.updateThumbnailUrl(uploadedFile.fileUrl()); } diff --git a/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java b/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java index 3b19cecfa..69d3426a2 100644 --- a/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java +++ b/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java @@ -1,12 +1,12 @@ package com.example.solidconnection.s3.config; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3Client; -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; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; @Configuration public class AmazonS3Config { @@ -21,12 +21,12 @@ public class AmazonS3Config { private String region; @Bean - public AmazonS3Client amazonS3Client() { - BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); - return (AmazonS3Client) AmazonS3ClientBuilder - .standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(credentials)) + public S3Client s3Client() { + AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); + + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build(); } } diff --git a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java index 98b0574f9..e66d36676 100644 --- a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java @@ -1,7 +1,7 @@ package com.example.solidconnection.s3.controller; import com.example.solidconnection.common.resolver.AuthorizedUser; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.dto.urlPrefixResponse; import com.example.solidconnection.s3.service.S3Service; @@ -39,7 +39,7 @@ public class S3Controller { public ResponseEntity uploadPreProfileImage( @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.PROFILE); return ResponseEntity.ok(profileImageUrl); } @@ -48,7 +48,7 @@ public ResponseEntity uploadPostProfileImage( @AuthorizedUser long siteUserId, @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.PROFILE); s3Service.deleteExProfile(siteUserId); return ResponseEntity.ok(profileImageUrl); } @@ -57,7 +57,7 @@ public ResponseEntity uploadPostProfileImage( public ResponseEntity uploadGpaImage( @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.GPA); return ResponseEntity.ok(profileImageUrl); } @@ -65,15 +65,15 @@ public ResponseEntity uploadGpaImage( public ResponseEntity uploadLanguageImage( @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.LANGUAGE_TEST); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/chat") - public ResponseEntity> uploadChatImage( - @RequestParam("files") List imageFiles + public ResponseEntity> uploadChatFile( + @RequestParam("files") List files ) { - List chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT); + List chatImageUrls = s3Service.uploadFiles(files, UploadType.CHAT); return ResponseEntity.ok(chatImageUrls); } diff --git a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java b/src/main/java/com/example/solidconnection/s3/domain/UploadType.java similarity index 79% rename from src/main/java/com/example/solidconnection/s3/domain/ImgType.java rename to src/main/java/com/example/solidconnection/s3/domain/UploadType.java index b26d5fc10..89c563d67 100644 --- a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java +++ b/src/main/java/com/example/solidconnection/s3/domain/UploadType.java @@ -3,19 +3,19 @@ import lombok.Getter; @Getter -public enum ImgType { +public enum UploadType { PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news"), - CHAT("chat"), + CHAT("chat/files"), MENTOR_PROOF("mentor-proof"), ; private final String type; - ImgType(String type) { + UploadType(String type) { this.type = type; } } diff --git a/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java b/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java index 51ef4caa9..568ef9a73 100644 --- a/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java +++ b/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java @@ -1,51 +1,52 @@ -package com.example.solidconnection.s3.service; + package com.example.solidconnection.s3.service; -import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION; -import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION; + import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION; + import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.SdkClientException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.CannedAccessControlList; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.example.solidconnection.common.exception.CustomException; -import java.io.IOException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; + import com.example.solidconnection.common.exception.CustomException; + import java.io.IOException; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import org.springframework.scheduling.annotation.Async; + import org.springframework.stereotype.Component; + import org.springframework.web.multipart.MultipartFile; + import software.amazon.awssdk.core.exception.SdkException; + import software.amazon.awssdk.core.sync.RequestBody; + import software.amazon.awssdk.services.s3.S3Client; + import software.amazon.awssdk.services.s3.model.ObjectCannedACL; + import software.amazon.awssdk.services.s3.model.PutObjectRequest; + import software.amazon.awssdk.services.s3.model.S3Exception; -@Component -@EnableAsync -@Slf4j -public class FileUploadService { + @Component + @Slf4j + @RequiredArgsConstructor + public class FileUploadService { - private final AmazonS3Client amazonS3; + private final S3Client s3Client; - public FileUploadService(AmazonS3Client amazonS3) { - this.amazonS3 = amazonS3; - } + @Async + public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) { + try { + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType(multipartFile.getContentType()) + .contentLength(multipartFile.getSize()) + .build(); - @Async - public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) { - // 메타데이터 생성 - String contentType = multipartFile.getContentType(); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(multipartFile.getSize()); + s3Client.putObject(putObjectRequest, + RequestBody.fromInputStream(multipartFile.getInputStream(), multipartFile.getSize())); - try { - amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); - log.info("이미지 업로드 정상적 완료 thread: {}", Thread.currentThread().getName()); - } catch (AmazonServiceException e) { - log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); - throw new CustomException(S3_SERVICE_EXCEPTION); - } catch (SdkClientException | IOException e) { - log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); - throw new CustomException(S3_CLIENT_EXCEPTION); + log.info("파일 업로드 정상 완료 thread: {}", Thread.currentThread().getName()); + } catch (S3Exception e) { + String errorMessage = (e.awsErrorDetails() != null) + ? e.awsErrorDetails().errorMessage() + : e.getMessage(); + log.error("S3 서비스 예외 발생 : {}", errorMessage); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkException | IOException e) { + log.error("S3 클라이언트 또는 IO 예외 발생 : {}", e.getMessage()); + throw new CustomException(S3_CLIENT_EXCEPTION); + } } } -} diff --git a/src/main/java/com/example/solidconnection/s3/service/S3Service.java b/src/main/java/com/example/solidconnection/s3/service/S3Service.java index 4c4110693..637821203 100644 --- a/src/main/java/com/example/solidconnection/s3/service/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/service/S3Service.java @@ -7,12 +7,8 @@ import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.SdkClientException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -23,21 +19,22 @@ import java.util.Objects; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; @Service @RequiredArgsConstructor public class S3Service { - private static final Logger log = LoggerFactory.getLogger(S3Service.class); private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5; - private final AmazonS3Client amazonS3; + private final S3Client s3Client; private final SiteUserRepository siteUserRepository; private final FileUploadService fileUploadService; private final ThreadPoolTaskExecutor asyncExecutor; @@ -55,30 +52,29 @@ public class S3Service { * - 5mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다. * - 5mb 미만의 파일은 바로 업로드한다. * */ - public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) { - // 파일 검증 + public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, UploadType uploadType) { validateImgFile(multipartFile); - // 파일 이름 생성 UUID randomUUID = UUID.randomUUID(); - String fileName = imageFile.getType() + "/" + randomUUID; - // 파일업로드 비동기로 진행 - if (multipartFile.getSize() >= MAX_FILE_SIZE_MB) { - asyncExecutor.submit(() -> { - fileUploadService.uploadFile(bucket, "origin/" + fileName, multipartFile); - }); - } else { - asyncExecutor.submit(() -> { - fileUploadService.uploadFile(bucket, fileName, multipartFile); - }); - } - return new UploadedFileUrlResponse(fileName); + String extension = getFileExtension(Objects.requireNonNull(multipartFile.getOriginalFilename())); + String baseFileName = randomUUID + "." + extension; + String fileName = uploadType.getType() + "/" + baseFileName; + final boolean isLargeFile = multipartFile.getSize() >= MAX_FILE_SIZE_MB && uploadType != UploadType.CHAT; + + final String uploadPath = isLargeFile ? "original/" + fileName : fileName; + final String returnPath = isLargeFile + ? "resize/" + fileName.substring(0, fileName.lastIndexOf('.')) + ".webp" + : fileName; + + fileUploadService.uploadFile(bucket, uploadPath, multipartFile); + + return new UploadedFileUrlResponse(returnPath); } - public List uploadFiles(List multipartFile, ImgType imageFile) { + public List uploadFiles(List multipartFile, UploadType uploadType) { List uploadedFileUrlResponseList = new ArrayList<>(); for (MultipartFile file : multipartFile) { - UploadedFileUrlResponse uploadedFileUrlResponse = uploadFile(file, imageFile); + UploadedFileUrlResponse uploadedFileUrlResponse = uploadFile(file, uploadType); uploadedFileUrlResponseList.add(uploadedFileUrlResponse); } return uploadedFileUrlResponseList; @@ -125,12 +121,14 @@ public void deletePostImage(String url) { private void deleteFile(String fileName) { try { - amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); - } catch (AmazonServiceException e) { - log.error("파일 삭제 중 s3 서비스 예외 발생 : {}", e.getMessage()); + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .build(); + s3Client.deleteObject(deleteObjectRequest); + } catch (S3Exception e) { throw new CustomException(S3_SERVICE_EXCEPTION); - } catch (SdkClientException e) { - log.error("파일 삭제 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); + } catch (SdkException e) { throw new CustomException(S3_CLIENT_EXCEPTION); } } diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index f16951d49..09823bbfb 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -5,7 +5,7 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.score.domain.GpaScore; @@ -40,7 +40,7 @@ public class ScoreService { public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, MultipartFile file) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.GPA); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadType.GPA); Gpa gpa = new Gpa(gpaScoreRequest.gpa(), gpaScoreRequest.gpaCriteria(), uploadedFile.fileUrl()); GpaScore newGpaScore = new GpaScore(gpa, siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); @@ -51,7 +51,7 @@ public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, Mul public Long submitLanguageTestScore(long siteUserId, LanguageTestScoreRequest languageTestScoreRequest, MultipartFile file) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.LANGUAGE_TEST); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadType.LANGUAGE_TEST); LanguageTest languageTest = new LanguageTest(languageTestScoreRequest.languageTestType(), languageTestScoreRequest.languageTestScore(), uploadedFile.fileUrl()); LanguageTestScore newScore = new LanguageTestScore(languageTest, siteUser); diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java index 6e8b88b66..94b524287 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -14,7 +14,7 @@ import com.example.solidconnection.location.country.repository.CountryRepository; import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.mentor.repository.MentorRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.AuthType; @@ -90,9 +90,8 @@ public void updateMyPageInfo(long siteUserId, MultipartFile imageFile, String ni user.setNickname(nickname); user.setNicknameModifiedAt(LocalDateTime.now()); } - if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadType.PROFILE); if (!isDefaultProfileImage(user.getProfileImageUrl())) { s3Service.deleteExProfile(user.getId()); } diff --git a/src/main/resources/secret b/src/main/resources/secret index 29524e2d6..1f93968a8 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 29524e2d6dad2042400de0370a11893029aacff2 +Subproject commit 1f93968a8475d4545d90e8f681b96382d25586af diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index f5ec202bb..4f53fcd37 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -454,8 +454,8 @@ class 채팅_이미지를_전송한다 { private SiteUser sender; private ChatParticipant senderParticipant; private ChatRoom chatRoom; - private static final String TEST_IMAGE_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example.jpg"; - private static final String TEST_IMAGE_URL2 = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example2.jpg"; + private static final String TEST_IMAGE_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/files/example.jpg"; + private static final String TEST_IMAGE_URL2 = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/files/example2.jpg"; private static final String EXPECTED_THUMBNAIL_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/thumbnails/example_thumb.jpg"; @BeforeEach diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java index 36211c341..bd2b7bfd0 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java @@ -25,7 +25,7 @@ import com.example.solidconnection.community.post.fixture.PostFixture; import com.example.solidconnection.community.post.fixture.PostImageFixture; import com.example.solidconnection.community.post.repository.PostRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -109,7 +109,7 @@ class 게시글_생성_테스트 { PostCreateRequest request = createPostCreateRequest(PostCategory.자유.name()); List imageFiles = List.of(createImageFile()); String expectedImageUrl = "test-image-url"; - given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + given(s3Service.uploadFiles(any(), eq(UploadType.COMMUNITY))) .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); // when @@ -179,7 +179,7 @@ class 게시글_수정_테스트 { PostUpdateRequest request = createPostUpdateRequest(); List imageFiles = List.of(createImageFile()); - given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + given(s3Service.uploadFiles(any(), eq(UploadType.COMMUNITY))) .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); // when diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java index daa429fc3..aa22ca191 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java @@ -13,7 +13,7 @@ import com.example.solidconnection.mentor.dto.MentorApplicationRequest; import com.example.solidconnection.mentor.fixture.MentorApplicationFixture; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.ExchangeStatus; @@ -69,7 +69,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when @@ -87,7 +87,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when @@ -105,7 +105,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when & then @@ -122,7 +122,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when & then @@ -173,7 +173,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when diff --git a/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java b/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java index f82a3bd84..91658334f 100644 --- a/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java @@ -18,7 +18,7 @@ import com.example.solidconnection.news.dto.NewsUpdateRequest; import com.example.solidconnection.news.fixture.NewsFixture; import com.example.solidconnection.news.repository.NewsRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -71,7 +71,7 @@ class 소식지_생성_테스트 { NewsCreateRequest request = createNewsCreateRequest(); MultipartFile imageFile = createImageFile(); String expectedImageUrl = "news/5a02ba2f-38f5-4ae9-9a24-53d624a18233"; - given(s3Service.uploadFile(any(), eq(ImgType.NEWS))) + given(s3Service.uploadFile(any(), eq(UploadType.NEWS))) .willReturn(new UploadedFileUrlResponse(expectedImageUrl)); // when @@ -110,7 +110,7 @@ void setUp() { String expectedUrl = "https://youtu.be/test-edit"; MultipartFile expectedFile = createImageFile(); String expectedNewImageUrl = "news/5a02ba2f-38f5-4ae9-9a24-53d624a18233-edit"; - given(s3Service.uploadFile(any(), eq(ImgType.NEWS))) + given(s3Service.uploadFile(any(), eq(UploadType.NEWS))) .willReturn(new UploadedFileUrlResponse(expectedNewImageUrl)); NewsUpdateRequest request = createNewsUpdateRequest( expectedTitle, @@ -185,7 +185,7 @@ void setUp() { assertAll( () -> assertThat(savedNews.getThumbnailUrl()).isEqualTo(newsProperties.defaultThumbnailUrl()), () -> then(s3Service).should().deletePostImage(CUSTOM_IMAGE_URL), - () -> then(s3Service).should(never()).uploadFile(null, ImgType.NEWS) + () -> then(s3Service).should(never()).uploadFile(null, UploadType.NEWS) ); } @@ -194,7 +194,7 @@ void setUp() { // given MultipartFile newImageFile = createImageFile(); String newImageUrl = "news/new-image-url"; - given(s3Service.uploadFile(newImageFile, ImgType.NEWS)) + given(s3Service.uploadFile(newImageFile, UploadType.NEWS)) .willReturn(new UploadedFileUrlResponse(newImageUrl)); NewsUpdateRequest request = createNewsUpdateRequest( null, @@ -248,7 +248,7 @@ void setUp() { assertAll( () -> assertThat(savedNews.getThumbnailUrl()).isEqualTo(newsProperties.defaultThumbnailUrl()), () -> then(s3Service).should(never()).deletePostImage(newsProperties.defaultThumbnailUrl()), - () -> then(s3Service).should(never()).uploadFile(null, ImgType.NEWS) + () -> then(s3Service).should(never()).uploadFile(null, UploadType.NEWS) ); } @@ -257,7 +257,7 @@ void setUp() { // given MultipartFile newImageFile = createImageFile(); String newImageUrl = "news/new-image-url"; - given(s3Service.uploadFile(newImageFile, ImgType.NEWS)) + given(s3Service.uploadFile(newImageFile, UploadType.NEWS)) .willReturn(new UploadedFileUrlResponse(newImageUrl)); NewsUpdateRequest request = createNewsUpdateRequest(null, null, null, null); diff --git a/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java b/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java new file mode 100644 index 000000000..59c024952 --- /dev/null +++ b/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java @@ -0,0 +1,124 @@ +package com.example.solidconnection.s3.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.s3.domain.UploadType; +import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import software.amazon.awssdk.services.s3.S3Client; + +@DisplayName("S3 서비스 테스트") +@ExtendWith(MockitoExtension.class) +public class S3ServiceTest { + + @InjectMocks + private S3Service s3Service; + + @Mock + private S3Client s3Client; + + @Mock + private FileUploadService fileUploadService; + + @Mock + private ThreadPoolTaskExecutor asyncExecutor; + + private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5; + + private MockMultipartFile createMockFile(String originalName, long size) { + return new MockMultipartFile("file", originalName, "image/jpeg", new byte[(int) size]); + } + + @Nested + class 파일_업로드_경로_및_리사이징_로직 { + + @Test + void O5MB_미만의_이미지는_원본_확장자를_유지하며_업로드된다() { + // given + MockMultipartFile file = createMockFile("test.png", MAX_FILE_SIZE_MB - 100); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile(file, UploadType.PROFILE); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("profile/"), + () -> assertThat(response.fileUrl()).endsWith(".png"), + () -> assertThat(response.fileUrl()).doesNotContain("original/", "resize/") + ); + } + + @Test + void O5MB_이상의_이미지는_original_경로로_업로드되고_resize_webp_경로를_반환한다() { + // given + MockMultipartFile file = createMockFile("test.jpg", MAX_FILE_SIZE_MB + 100); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile(file, UploadType.PROFILE); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("resize/profile/"), + () -> assertThat(response.fileUrl()).endsWith(".webp") + ); + } + + @Test + void 채팅_파일은_5MB가_넘어도_리사이징_경로를_적용하지_않고_원본_경로를_반환한다() { + // given + MockMultipartFile file = createMockFile("chat.jpg", MAX_FILE_SIZE_MB + 100); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile(file, UploadType.CHAT); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("chat/files/"), + () -> assertThat(response.fileUrl()).endsWith(".jpg"), + () -> assertThat(response.fileUrl()).doesNotContain("resize/") + ); + } + } + + @Nested + class 파일_검증 { + + @Test + void 허용되지_않은_확장자의_파일은_예외를_던진다() { + // given + MockMultipartFile invalidFile = createMockFile("virus.exe", 100); + + // when & then + assertThatThrownBy(() -> s3Service.uploadFile(invalidFile, UploadType.PROFILE)) + .isInstanceOf(CustomException.class) + .hasMessageContaining("허용된 형식"); + } + + @Test + void 채팅_업로드시_이미지_외의_허용된_문서_확장자들도_성공적으로_검증을_통과한다() { + // given + MockMultipartFile pdfFile = createMockFile("test.pdf", 100); + MockMultipartFile wordFile = createMockFile("test.docx", 100); + + // when & then + assertAll( + () -> assertThatCode(() -> s3Service.uploadFile(pdfFile, UploadType.CHAT)) + .doesNotThrowAnyException(), + () -> assertThatCode(() -> s3Service.uploadFile(wordFile, UploadType.CHAT)) + .doesNotThrowAnyException() + ); + } + } +} diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 8760a645b..a6acc6617 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -4,7 +4,7 @@ import static org.mockito.BDDMockito.given; import com.example.solidconnection.common.VerifyStatus; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.score.domain.GpaScore; @@ -115,7 +115,7 @@ void setUp() { GpaScoreRequest request = createGpaScoreRequest(); MockMultipartFile file = createFile(); String fileUrl = "/gpa-report.pdf"; - given(s3Service.uploadFile(file, ImgType.GPA)).willReturn(new UploadedFileUrlResponse(fileUrl)); + given(s3Service.uploadFile(file, UploadType.GPA)).willReturn(new UploadedFileUrlResponse(fileUrl)); // when long scoreId = scoreService.submitGpaScore(user.getId(), request, file); @@ -131,7 +131,7 @@ void setUp() { LanguageTestScoreRequest request = createLanguageTestScoreRequest(); MockMultipartFile file = createFile(); String fileUrl = "/gpa-report.pdf"; - given(s3Service.uploadFile(file, ImgType.LANGUAGE_TEST)).willReturn(new UploadedFileUrlResponse(fileUrl)); + given(s3Service.uploadFile(file, UploadType.LANGUAGE_TEST)).willReturn(new UploadedFileUrlResponse(fileUrl)); // when long scoreId = scoreService.submitLanguageTestScore(user.getId(), request, file); diff --git a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java index 3a82681f3..52a530d44 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java @@ -25,7 +25,7 @@ import com.example.solidconnection.location.region.fixture.RegionFixture; import com.example.solidconnection.location.region.repository.InterestedRegionRepository; import com.example.solidconnection.mentor.fixture.MentorFixture; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.AuthType; @@ -209,7 +209,7 @@ class 프로필_이미지_수정_테스트 { // given String expectedUrl = "newProfileImageUrl"; MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadType.PROFILE))) .willReturn(new UploadedFileUrlResponse(expectedUrl)); // when @@ -224,7 +224,7 @@ class 프로필_이미지_수정_테스트 { void 프로필을_처음_수정하는_것이면_이전_이미지를_삭제하지_않는다() { // given MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadType.PROFILE))) .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when @@ -239,7 +239,7 @@ class 프로필_이미지_수정_테스트 { // given SiteUser 커스텀_프로필_사용자 = createSiteUserWithCustomProfile(); MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadType.PROFILE))) .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when @@ -255,7 +255,7 @@ class 닉네임_수정_테스트 { @BeforeEach void setUp() { - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadType.PROFILE))) .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); }