diff --git a/build.gradle b/build.gradle index 7cd5ffdc..1cab0c52 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,10 @@ dependencies { //S3 관련 의존성 부여 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + //webp 변환 라이브러리 + implementation "com.sksamuel.scrimage:scrimage-core:4.0.32" + implementation "com.sksamuel.scrimage:scrimage-webp:4.0.32" + implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.32.0' implementation 'io.jsonwebtoken:jjwt:0.9.1' diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java index 1d21b8d1..1d8990d8 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java @@ -8,6 +8,8 @@ public record AddSessionImageRequest( @NotNull Long sessionId, @NotNull - MultipartFile image + MultipartFile image, + @NotNull + Integer order ) { } diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index 11fc980e..7d3a27b6 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -52,13 +52,13 @@ public enum ErrorCode { EDUCATION_STATUS_NOT_BEFORE(HttpStatus.BAD_REQUEST, "E-402", "이미 시작한 적이 있는 교육입니다."), MEMBER_CANT_ACCESS(HttpStatus.BAD_REQUEST, "E-403", "해당 멤버의 ROLE로 접근할 수 없습니다"), - //세션 사진 - SESSION_IMAGE_COUNT_MISMATCH(HttpStatus.BAD_REQUEST, "P-101", "저장된 사진 수와 요청 사진 수가 다릅니다."), - SESSION_ORDER_INVALID(HttpStatus.BAD_REQUEST, "P-102", "입력한 순서는 유효하지 않습니다."), - FILE_EXTENSION_FAULT(HttpStatus.BAD_REQUEST, "F-001", "해당 파일 확장자 명이 존재하지 않습니다."), FILE_IS_EMPTY(HttpStatus.BAD_REQUEST, "F-002", "파일이 비어있습니다"), + //세션 사진 + SESSION_IMAGE_COUNT_MISMATCH(HttpStatus.BAD_REQUEST, "IM-101", "저장된 사진 수와 요청 사진 수가 다릅니다."), + SESSION_ORDER_INVALID(HttpStatus.BAD_REQUEST, "IM-102", "입력한 순서는 유효하지 않습니다."), + INVALID_ANSWER(HttpStatus.BAD_REQUEST, "Q-101", "객관식 문제는 숫자 형식의 값만 정답으로 추가할 수 있습니다."), CONTENT_IS_NOT_ANSWER(HttpStatus.BAD_REQUEST, "Q-201", "추가되지 않은 정답을 추가할 수 없습니다."), QUIZ_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "Q-301", "퀴즈 번호는 중복될 수 없습니다."), @@ -93,6 +93,7 @@ public enum ErrorCode { SCORER_LOCK_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-006", "득점자 락 획득 과정에서 에러 발생"), IMAGE_CONVERT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-007", "로컬 이미지 변환에 실패했습니다"), SSE_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-008", "서버 이벤트 전송간 오류 발생"), + WEBP_CONVERT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-009", "webp 변환에 실패했습니다") ; private final HttpStatus httpStatus; diff --git a/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java b/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java index 117f94f7..75db8840 100644 --- a/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java +++ b/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java @@ -1,6 +1,6 @@ package org.cotato.csquiz.common.s3; -import static org.cotato.csquiz.common.util.FileUtil.extractFileExtension; +import static org.cotato.csquiz.common.util.FileUtil.convert; import static org.cotato.csquiz.common.util.FileUtil.isImageFileExtension; import com.amazonaws.services.s3.AmazonS3Client; @@ -8,9 +8,7 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import org.cotato.csquiz.common.entity.S3Info; -import org.cotato.csquiz.common.error.ErrorCode; import org.cotato.csquiz.common.error.exception.ImageException; -import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -18,8 +16,6 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; @Slf4j @RequiredArgsConstructor @@ -36,13 +32,17 @@ public S3Info uploadFiles(MultipartFile multipartFile, String folderName) throws log.info("{} 사진 업로드", multipartFile.getOriginalFilename()); File localUploadFile = convert(multipartFile); - String fileName = folderName + "/" + localUploadFile.getName(); - String uploadUrl = putS3(localUploadFile, fileName); - localUploadFile.delete(); + return uploadFiles(localUploadFile, folderName); + } + + public S3Info uploadFiles(File file, String folderName) { + String fileName = folderName + "/" + file.getName(); + String uploadUrl = putS3(file, fileName); + file.delete(); return S3Info.builder() .folderName(folderName) - .fileName(localUploadFile.getName()) + .fileName(file.getName()) .url(uploadUrl) .build(); } @@ -74,19 +74,4 @@ private boolean isImageFile(File file) { return isImageFileExtension(extension); } - - private File convert(MultipartFile file) throws ImageException { - String fileExtension = extractFileExtension(file); - File convertFile = new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + "." + fileExtension); - - try { - FileOutputStream fos = new FileOutputStream(convertFile); - fos.write(file.getBytes()); - fos.close(); - - return convertFile; - } catch (IOException e) { - throw new ImageException(ErrorCode.IMAGE_CONVERT_FAIL); - } - } } diff --git a/src/main/java/org/cotato/csquiz/common/util/FileUtil.java b/src/main/java/org/cotato/csquiz/common/util/FileUtil.java index efed8113..40267f1f 100644 --- a/src/main/java/org/cotato/csquiz/common/util/FileUtil.java +++ b/src/main/java/org/cotato/csquiz/common/util/FileUtil.java @@ -1,6 +1,12 @@ package org.cotato.csquiz.common.util; +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.webp.WebpWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.List; +import java.util.UUID; import org.cotato.csquiz.common.error.ErrorCode; import org.cotato.csquiz.common.error.exception.ImageException; import org.springframework.web.multipart.MultipartFile; @@ -21,4 +27,34 @@ public static String extractFileExtension(MultipartFile file) throws ImageExcept public static boolean isImageFileExtension(String fileExtension) { return IMAGE_FILE_EXTENSIONS.contains(fileExtension.toLowerCase()); } + + public static File convert(MultipartFile file) throws ImageException { + String fileExtension = extractFileExtension(file); + File convertFile = new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + "." + fileExtension); + + try { + FileOutputStream fos = new FileOutputStream(convertFile); + fos.write(file.getBytes()); + fos.close(); + + return convertFile; + } catch (IOException e) { + throw new ImageException(ErrorCode.IMAGE_CONVERT_FAIL); + } + } + + public static File convertToWebp(File originalFile) throws ImageException { + try { + String fileNameWithoutExtension = originalFile.getName().replaceFirst("[.][^.]+$", ""); + + File webpFile = ImmutableImage.loader() + .fromFile(originalFile) + .output(WebpWriter.DEFAULT, new File(fileNameWithoutExtension + ".webp")); + + originalFile.delete(); + return webpFile; + } catch (IOException e) { + throw new ImageException(ErrorCode.WEBP_CONVERT_FAIL); + } + } } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java index c6e87a17..9f448ffe 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java @@ -1,7 +1,6 @@ package org.cotato.csquiz.domain.generation.repository; import java.util.List; -import java.util.Optional; import org.cotato.csquiz.domain.generation.entity.Session; import org.cotato.csquiz.domain.generation.entity.SessionImage; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,5 +10,5 @@ public interface SessionImageRepository extends JpaRepository findAllBySessionIn(List sessions); - Optional findFirstBySessionOrderByOrderDesc(Session session); + boolean existsBySessionAndOrder(Session session, Integer order); } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java index 2921671c..622f6683 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java @@ -1,5 +1,8 @@ package org.cotato.csquiz.domain.generation.service; +import static org.cotato.csquiz.common.util.FileUtil.*; + +import java.io.File; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -21,14 +24,16 @@ public class ProjectImageService { private final ProjectImageRepository projectImageRepository; @Transactional - public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNameImage) + public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNailImage) throws ImageException { List newImages = new ArrayList<>(); - S3Info logoImageInfo = s3Uploader.uploadFiles(logoImage, PROJECT_IMAGE); + File webpLogoImage = convertToWebp(convert(logoImage)); + S3Info logoImageInfo = s3Uploader.uploadFiles(webpLogoImage, PROJECT_IMAGE); newImages.add(ProjectImage.logoImage(logoImageInfo, projectId)); - S3Info thumbNailInfo = s3Uploader.uploadFiles(thumbNameImage, PROJECT_IMAGE); + File webpThumbNailImage = convertToWebp(convert(thumbNailImage)); + S3Info thumbNailInfo = s3Uploader.uploadFiles(webpThumbNailImage, PROJECT_IMAGE); newImages.add(ProjectImage.thumbNailImage(thumbNailInfo, projectId)); projectImageRepository.saveAll(newImages); diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java index 2318caeb..1ad1ea04 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java @@ -67,13 +67,14 @@ public AddSessionImageResponse additionalSessionImage(AddSessionImageRequest req S3Info imageInfo = s3Uploader.uploadFiles(request.image(), SESSION_BUCKET_DIRECTORY); - Integer imageOrder = sessionImageRepository.findFirstBySessionOrderByOrderDesc(session) - .map(sessionImage -> sessionImage.getOrder() + 1).orElse(0); + if (sessionImageRepository.existsBySessionAndOrder(session, request.order())) { + throw new AppException(ErrorCode.SESSION_ORDER_INVALID); + } SessionImage sessionImage = SessionImage.builder() .session(session) .s3Info(imageInfo) - .order(imageOrder) + .order(request.order()) .build(); return AddSessionImageResponse.from(sessionImageRepository.save(sessionImage));