diff --git a/src/main/java/leets/bookmark/domain/file/application/dto/response/FetchedThumbnailResponse.java b/src/main/java/leets/bookmark/domain/file/application/dto/response/FetchedThumbnailResponse.java new file mode 100644 index 00000000..024e7d2a --- /dev/null +++ b/src/main/java/leets/bookmark/domain/file/application/dto/response/FetchedThumbnailResponse.java @@ -0,0 +1,11 @@ +package leets.bookmark.domain.file.application.dto.response; + +import lombok.Builder; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; + +@Builder +public record FetchedThumbnailResponse( + InputStreamResource inputStreamResource, + MediaType mediaType +) {} diff --git a/src/main/java/leets/bookmark/domain/file/application/exception/FileErrorCode.java b/src/main/java/leets/bookmark/domain/file/application/exception/FileErrorCode.java index 89c3afac..5ccc7a3f 100644 --- a/src/main/java/leets/bookmark/domain/file/application/exception/FileErrorCode.java +++ b/src/main/java/leets/bookmark/domain/file/application/exception/FileErrorCode.java @@ -11,7 +11,8 @@ public enum FileErrorCode implements ErrorCode { FILE_NOT_FOUND_EXCEPTION(404,"해당하는 파일을 찾을 수 없습니다."), INVALID_FILE_EXTENSION(400, "올바르지 않은 파일 확장자입니다."), FILE_OWNER_MISMATCH_EXCEPTION(403, "파일 소유자만 접근 가능합니다."), - S3_UPLOAD_EXCEPTION(500, "파일 업로드에 실패하였습니다."); + S3_UPLOAD_EXCEPTION(500, "파일 업로드에 실패하였습니다."), + IMAGE_FETCH_EXCEPTION(500, "이미지를 가져올 수 없습니다."); private final int errorCode; private final String message; diff --git a/src/main/java/leets/bookmark/domain/file/application/exception/ImageFetchException.java b/src/main/java/leets/bookmark/domain/file/application/exception/ImageFetchException.java new file mode 100644 index 00000000..e0b56891 --- /dev/null +++ b/src/main/java/leets/bookmark/domain/file/application/exception/ImageFetchException.java @@ -0,0 +1,9 @@ +package leets.bookmark.domain.file.application.exception; + +import leets.bookmark.global.common.exception.BusinessException; + +public class ImageFetchException extends BusinessException { + public ImageFetchException(){ + super(FileErrorCode.IMAGE_FETCH_EXCEPTION); + } +} diff --git a/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java b/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java index e0586d82..ebde563f 100644 --- a/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java +++ b/src/main/java/leets/bookmark/domain/file/application/mapper/FileMapper.java @@ -2,10 +2,13 @@ import leets.bookmark.domain.bookmark.domain.entity.Bookmark; import leets.bookmark.domain.file.application.dto.request.FileSaveRequest; +import leets.bookmark.domain.file.application.dto.response.FetchedThumbnailResponse; import leets.bookmark.domain.file.application.dto.response.FileResponse; import leets.bookmark.domain.file.domain.entity.File; import leets.bookmark.domain.file.domain.entity.enums.FileType; import leets.bookmark.domain.user.domain.entity.User; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @Component @@ -41,4 +44,12 @@ public FileResponse toFileResponse(File file){ .updatedAt(file.getUpdatedAt()) .build(); } + + public FetchedThumbnailResponse toFetchedThumbnailResponse(InputStreamResource inputStreamResource, + MediaType mediaType){ + return FetchedThumbnailResponse.builder() + .inputStreamResource(inputStreamResource) + .mediaType(mediaType) + .build(); + } } diff --git a/src/main/java/leets/bookmark/domain/file/application/usecase/FileUseCase.java b/src/main/java/leets/bookmark/domain/file/application/usecase/FileUseCase.java index fc731be3..22e50b0f 100644 --- a/src/main/java/leets/bookmark/domain/file/application/usecase/FileUseCase.java +++ b/src/main/java/leets/bookmark/domain/file/application/usecase/FileUseCase.java @@ -4,9 +4,11 @@ import leets.bookmark.domain.bookmark.domain.entity.Bookmark; import leets.bookmark.domain.file.application.dto.request.FileSaveRequest; import leets.bookmark.domain.file.application.dto.request.FileUpdateRequest; +import leets.bookmark.domain.file.application.dto.response.FetchedThumbnailResponse; import leets.bookmark.domain.file.application.dto.response.FileResponse; import leets.bookmark.domain.file.application.dto.response.PresignedUrlResponse; import leets.bookmark.domain.file.application.exception.FileOwnerMismatchException; +import leets.bookmark.domain.file.application.exception.ImageFetchException; import leets.bookmark.domain.file.application.exception.InvalidFileExtensionException; import leets.bookmark.domain.file.application.mapper.FileMapper; import leets.bookmark.domain.file.application.mapper.PreSignedMapper; @@ -16,10 +18,20 @@ import leets.bookmark.domain.user.domain.entity.User; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.Optional; + +@Slf4j @Validated @Service @RequiredArgsConstructor @@ -99,21 +111,40 @@ public void deleteFile(User user, Bookmark bookmark) { fileDeleteService.delete(file); } - private void validateFileOwner(User user, File file){ - if(!(user.getId().equals(file.getUser().getId()))){ - throw new FileOwnerMismatchException(); + public FetchedThumbnailResponse getThumbnailImage(String thumbnailUrl) { + try { + String fileName = extractFileNameWithExtension(thumbnailUrl); + FileType fileType = getValidatedFileType(thumbnailUrl); + + URI uri = URI.create(thumbnailUrl); + + URL url = new URL(uri.toASCIIString()); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + InputStream inputStream = connection.getInputStream(); + + String contentType = Optional.ofNullable(connection.getContentType()) + .orElse("application/octet-stream"); + MediaType mediaType = MediaType.parseMediaType(contentType); + + return fileMapper.toFetchedThumbnailResponse(new InputStreamResource(inputStream), mediaType); + + } catch (Exception e){ + log.warn("썸네일 이미지를 가져오는 중 오류 발생. URL: {}", thumbnailUrl, e); + throw new ImageFetchException(); } } - private String getExtension(String fileName){ - if(fileName == null || !fileName.contains(".")){ - throw new InvalidFileExtensionException(); + private void validateFileOwner(User user, File file){ + if(!(user.getId().equals(file.getUser().getId()))){ + throw new FileOwnerMismatchException(); } - return fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); } private FileType getValidatedFileType(String fileName){ - return FileType.fromExtension(getExtension(getExtension(fileName))) + return FileType.fromFileName(fileName) .orElseThrow(InvalidFileExtensionException::new); } diff --git a/src/main/java/leets/bookmark/domain/file/domain/entity/enums/FileType.java b/src/main/java/leets/bookmark/domain/file/domain/entity/enums/FileType.java index a7878514..2ad4a839 100644 --- a/src/main/java/leets/bookmark/domain/file/domain/entity/enums/FileType.java +++ b/src/main/java/leets/bookmark/domain/file/domain/entity/enums/FileType.java @@ -24,15 +24,6 @@ public enum FileType { private final String extension; - public static Optional fromExtension(String ext){ - if(ext == null){ - return Optional.empty(); - } - return Stream.of(FileType.values()) - .filter(fileType -> fileType.getExtension().equalsIgnoreCase(ext)) - .findFirst(); - } - public static Optional fromFileName(String fileName) { return Stream.of(FileType.values()) .filter(fileType -> fileName.toLowerCase().contains(fileType.getExtension())) diff --git a/src/main/java/leets/bookmark/domain/file/presentation/FileController.java b/src/main/java/leets/bookmark/domain/file/presentation/FileController.java index 3f7a1f20..34e2c6c7 100644 --- a/src/main/java/leets/bookmark/domain/file/presentation/FileController.java +++ b/src/main/java/leets/bookmark/domain/file/presentation/FileController.java @@ -1,10 +1,13 @@ package leets.bookmark.domain.file.presentation; import io.swagger.v3.oas.annotations.tags.Tag; +import leets.bookmark.domain.file.application.dto.response.FetchedThumbnailResponse; import leets.bookmark.domain.file.application.dto.response.PresignedUrlResponse; import leets.bookmark.domain.file.application.usecase.FileUseCase; import leets.bookmark.global.common.response.CommonResponse; import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @Tag(name = "FILE", description = "파일 API") @@ -21,4 +24,12 @@ public CommonResponse getPresignedUrl(@RequestParam String return CommonResponse.createSuccess(FileResponseCode.PRE_SIGNED_URL_SUCCESS.getMessage(), response); } + @GetMapping("/thumbnail-image") + public ResponseEntity getThumbnailImage(@RequestParam String thumbnailUrl) { + FetchedThumbnailResponse response = fileUseCase.getThumbnailImage(thumbnailUrl); + return ResponseEntity.ok() + .contentType(response.mediaType()) + .body(response.inputStreamResource()); + } + }