From d73df072b559938610d5a1e66c2cf4cb2436691f Mon Sep 17 00:00:00 2001 From: Changyu Shin Date: Wed, 7 Aug 2024 18:31:43 +0900 Subject: [PATCH 1/4] =?UTF-8?q?GETP-170=20feat:=20`@Deprecated`=EB=90=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../storage/exception/ImageErrorCode.java | 29 ------------------- .../presentation/MyMemberControllerTest.java | 13 +-------- .../presentation/ErrorCodeController.java | 10 ------- 3 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 src/main/java/es/princip/getp/infra/storage/exception/ImageErrorCode.java diff --git a/src/main/java/es/princip/getp/infra/storage/exception/ImageErrorCode.java b/src/main/java/es/princip/getp/infra/storage/exception/ImageErrorCode.java deleted file mode 100644 index b252db23..00000000 --- a/src/main/java/es/princip/getp/infra/storage/exception/ImageErrorCode.java +++ /dev/null @@ -1,29 +0,0 @@ -package es.princip.getp.infra.storage.exception; - -import es.princip.getp.infra.exception.ErrorCode; -import es.princip.getp.infra.exception.ErrorDescription; -import org.springframework.http.HttpStatus; - -public enum ImageErrorCode implements ErrorCode { - - INVALID_IMAGE_FILE(HttpStatus.BAD_REQUEST, "유효하지 않은 이미지 파일 입니다."), - NOT_ALLOWED_EXTENSION(HttpStatus.CONFLICT, "허용되지 않는 확장자 입니다."), - IMAGE_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 저장에 실패하였습니다."), - IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 삭제에 실패하였습니다."); - - private final HttpStatus status; - private final ErrorDescription description; - - ImageErrorCode(HttpStatus status, String message) { - this.status = status; - this.description = ErrorDescription.of(this.name(), message); - } - - public HttpStatus status() { - return status; - } - - public ErrorDescription description() { - return description; - } -} diff --git a/src/test/java/es/princip/getp/domain/member/command/presentation/MyMemberControllerTest.java b/src/test/java/es/princip/getp/domain/member/command/presentation/MyMemberControllerTest.java index a7261e8f..32bd9ba3 100644 --- a/src/test/java/es/princip/getp/domain/member/command/presentation/MyMemberControllerTest.java +++ b/src/test/java/es/princip/getp/domain/member/command/presentation/MyMemberControllerTest.java @@ -3,8 +3,6 @@ import es.princip.getp.domain.member.command.application.MemberService; import es.princip.getp.domain.member.command.domain.model.MemberType; import es.princip.getp.infra.annotation.WithCustomMockUser; -import es.princip.getp.infra.presentation.ErrorCodeController; -import es.princip.getp.infra.storage.exception.ImageErrorCode; import es.princip.getp.infra.support.AbstractControllerTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -15,7 +13,6 @@ import static es.princip.getp.domain.member.fixture.ProfileImageFixture.profileImage; import static es.princip.getp.infra.storage.fixture.ImageStorageFixture.imageMultiPartFile; -import static es.princip.getp.infra.util.ErrorCodeFields.errorCodeFields; import static es.princip.getp.infra.util.FieldDescriptorHelper.getDescriptor; import static es.princip.getp.infra.util.HeaderDescriptorHelper.authorizationHeaderDescriptor; import static es.princip.getp.infra.util.PayloadDocumentationHelper.responseFields; @@ -25,11 +22,10 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest({MyMemberController.class, ErrorCodeController.class}) +@WebMvcTest(MyMemberController.class) class MyMemberControllerTest extends AbstractControllerTest { @MockBean @@ -64,12 +60,5 @@ public void uploadProfileImage() throws Exception { ) .andDo(print()); } - - @Test - void uploadProfileImageErrorCode() throws Exception { - mockMvc.perform(get("/error-code/storage/images")) - .andExpect(status().isOk()) - .andDo(restDocs.document(errorCodeFields(ImageErrorCode.values()))); - } } } \ No newline at end of file diff --git a/src/test/java/es/princip/getp/infra/presentation/ErrorCodeController.java b/src/test/java/es/princip/getp/infra/presentation/ErrorCodeController.java index 4246c9f2..f37cb7ff 100644 --- a/src/test/java/es/princip/getp/infra/presentation/ErrorCodeController.java +++ b/src/test/java/es/princip/getp/infra/presentation/ErrorCodeController.java @@ -4,7 +4,6 @@ import es.princip.getp.domain.auth.exception.SignUpErrorCode; import es.princip.getp.infra.dto.response.ErrorCodeResponse; import es.princip.getp.infra.exception.ErrorCode; -import es.princip.getp.infra.storage.exception.ImageErrorCode; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -33,13 +32,4 @@ public Map getSignUpErrorCode() { } return map; } - - @GetMapping("/storage/images") - public Map getImageStorageErrorCode() { - Map map = new HashMap<>(); - for (ErrorCode errorCode : ImageErrorCode.values()) { - map.put(errorCode.description().code(), ErrorCodeResponse.from(errorCode)); - } - return map; - } } \ No newline at end of file From bbe2fe6930d8eb5166458cce0230b78f8ef6410c Mon Sep 17 00:00:00 2001 From: Changyu Shin Date: Wed, 7 Aug 2024 18:33:45 +0900 Subject: [PATCH 2/4] =?UTF-8?q?GETP-170=20feat:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../handler/DefaultExceptionHandler.java | 2 +- .../getp/infra/storage/ImageStorage.java | 13 ++-- .../storage/application/FileStorage.java | 11 ++++ .../application/FileUploadLoggingService.java | 22 +++++++ .../application/FileUploadService.java | 56 +++++++++++++++++ .../getp/infra/storage/domain/FileLog.java | 33 ++++++++++ .../storage/domain/FileLogRepository.java | 8 +++ .../exception/FailedFileSaveException.java | 14 +++++ .../exception/FailedImageSaveException.java | 14 +++++ .../NotSupportedExtensionException.java | 14 +++++ .../infra/storage/infra/LocalFileStorage.java | 59 ++++++++++++++++++ .../presentation/FileStorageController.java | 36 +++++++++++ .../presentation/dto/FileUploadResponse.java | 6 ++ src/main/resources/application-dev.yml | 4 +- src/main/resources/application-local.yml | 4 +- test/storage/images/image.jpeg | Bin 95987 -> 0 bytes 17 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 src/main/java/es/princip/getp/infra/storage/application/FileStorage.java create mode 100644 src/main/java/es/princip/getp/infra/storage/application/FileUploadLoggingService.java create mode 100644 src/main/java/es/princip/getp/infra/storage/application/FileUploadService.java create mode 100644 src/main/java/es/princip/getp/infra/storage/domain/FileLog.java create mode 100644 src/main/java/es/princip/getp/infra/storage/domain/FileLogRepository.java create mode 100644 src/main/java/es/princip/getp/infra/storage/exception/FailedFileSaveException.java create mode 100644 src/main/java/es/princip/getp/infra/storage/exception/FailedImageSaveException.java create mode 100644 src/main/java/es/princip/getp/infra/storage/exception/NotSupportedExtensionException.java create mode 100644 src/main/java/es/princip/getp/infra/storage/infra/LocalFileStorage.java create mode 100644 src/main/java/es/princip/getp/infra/storage/presentation/FileStorageController.java create mode 100644 src/main/java/es/princip/getp/infra/storage/presentation/dto/FileUploadResponse.java delete mode 100644 test/storage/images/image.jpeg diff --git a/.gitignore b/.gitignore index 025b06d3..3ca595d8 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,7 @@ local.sh # Static Resources src/main/resources/static/* +src/test/resources/static/* storage/ # Yml File diff --git a/src/main/java/es/princip/getp/infra/exception/handler/DefaultExceptionHandler.java b/src/main/java/es/princip/getp/infra/exception/handler/DefaultExceptionHandler.java index 4961a6fa..2dad7631 100644 --- a/src/main/java/es/princip/getp/infra/exception/handler/DefaultExceptionHandler.java +++ b/src/main/java/es/princip/getp/infra/exception/handler/DefaultExceptionHandler.java @@ -23,7 +23,7 @@ public ResponseEntity handleHttpRequestMethodNotSupportedExcepti @ExceptionHandler(Exception.class) public ResponseEntity handleException(final Exception exception) { - log.info(exception.getMessage()); + log.error(exception.getMessage(), exception); return ApiErrorResponse.error(DefaultErrorCode.INTERNAL_SERVER_ERROR); } } \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/ImageStorage.java b/src/main/java/es/princip/getp/infra/storage/ImageStorage.java index 846e652d..975ee163 100644 --- a/src/main/java/es/princip/getp/infra/storage/ImageStorage.java +++ b/src/main/java/es/princip/getp/infra/storage/ImageStorage.java @@ -1,7 +1,6 @@ package es.princip.getp.infra.storage; -import es.princip.getp.infra.exception.BusinessLogicException; -import es.princip.getp.infra.storage.exception.ImageErrorCode; +import es.princip.getp.infra.storage.exception.FailedImageSaveException; import es.princip.getp.infra.util.ImageUtil; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -21,7 +20,7 @@ @Getter public class ImageStorage { - public ImageStorage(@Value("${spring.storage.path}") String storagePath) { + public ImageStorage(@Value("${spring.storage.local.path}") String storagePath) { this.storagePath = Paths.get(storagePath).normalize().toAbsolutePath(); this.imageStoragePath = Paths.get("images"); } @@ -55,7 +54,7 @@ public void deleteImage(Path destination) { Files.delete(this.storagePath.resolve(destination)); } } catch (IOException exception) { - throw new BusinessLogicException(ImageErrorCode.IMAGE_DELETE_FAILED); + throw new FailedImageSaveException(); } } @@ -75,7 +74,7 @@ private Path getAbsoluteImageStoragePath() { */ private void validateImage(InputStream imageStream) { if (!ImageUtil.isValidImage(imageStream)) { - throw new BusinessLogicException(ImageErrorCode.NOT_ALLOWED_EXTENSION); + throw new FailedImageSaveException(); } } @@ -101,7 +100,7 @@ private void copyImageToDestination(InputStream imageStream, Path destination) { makeDirectories(destination.getParent()); Files.copy(imageStream, destination, StandardCopyOption.REPLACE_EXISTING); } catch (IOException exception) { - throw new BusinessLogicException(ImageErrorCode.IMAGE_SAVE_FAILED); + throw new FailedImageSaveException(); } } @@ -114,7 +113,7 @@ private void makeDirectories(Path path) { File directory = new File(path.toUri()); if (!directory.exists()) { if (!directory.mkdirs()) { - throw new BusinessLogicException(ImageErrorCode.IMAGE_SAVE_FAILED); + throw new FailedImageSaveException(); } } } diff --git a/src/main/java/es/princip/getp/infra/storage/application/FileStorage.java b/src/main/java/es/princip/getp/infra/storage/application/FileStorage.java new file mode 100644 index 00000000..3754815f --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/application/FileStorage.java @@ -0,0 +1,11 @@ +package es.princip.getp.infra.storage.application; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; + +public interface FileStorage { + + URI storeFile(InputStream in, Path destination) throws IOException; +} diff --git a/src/main/java/es/princip/getp/infra/storage/application/FileUploadLoggingService.java b/src/main/java/es/princip/getp/infra/storage/application/FileUploadLoggingService.java new file mode 100644 index 00000000..70d0a780 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/application/FileUploadLoggingService.java @@ -0,0 +1,22 @@ +package es.princip.getp.infra.storage.application; + +import es.princip.getp.infra.storage.domain.FileLog; +import es.princip.getp.infra.storage.domain.FileLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class FileUploadLoggingService { + + private final FileLogRepository fileLogRepository; + + @Transactional + public FileLog logFile(final String fileName) { + final String converted = fileName.replace(" ", "_"); + final FileLog fileLog = new FileLog(converted); + fileLogRepository.save(fileLog); + return fileLog; + } +} diff --git a/src/main/java/es/princip/getp/infra/storage/application/FileUploadService.java b/src/main/java/es/princip/getp/infra/storage/application/FileUploadService.java new file mode 100644 index 00000000..3128b1e6 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/application/FileUploadService.java @@ -0,0 +1,56 @@ +package es.princip.getp.infra.storage.application; + +import es.princip.getp.infra.storage.domain.FileLog; +import es.princip.getp.infra.storage.exception.FailedFileSaveException; +import es.princip.getp.infra.storage.exception.NotSupportedExtensionException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class FileUploadService { + + private final FileStorage fileStorage; + private final FileUploadLoggingService fileUploadLoggingService; + + private static final Set ALLOWED_MIME_TYPES = Set.of( + "application/pdf", + "application/zip", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/x-hwp", + "image/jpeg", + "image/png" + ); + + public URI uploadFile(final MultipartFile file) { + validateContentType(file); + + if (file.getOriginalFilename() == null) { + throw new FailedFileSaveException(); + } + + final FileLog fileLog = fileUploadLoggingService.logFile(file.getOriginalFilename()); + final Path filePath = fileLog.getFilePath(); + + try (final InputStream in = file.getInputStream()) { + return fileStorage.storeFile(in, filePath); + } catch (Exception exception) { + throw new FailedFileSaveException(); + } + } + + private void validateContentType(final MultipartFile file) { + final String contentType = file.getContentType(); + + if (!ALLOWED_MIME_TYPES.contains(contentType)) { + throw new NotSupportedExtensionException(); + } + } +} diff --git a/src/main/java/es/princip/getp/infra/storage/domain/FileLog.java b/src/main/java/es/princip/getp/infra/storage/domain/FileLog.java new file mode 100644 index 00000000..6a028cc4 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/domain/FileLog.java @@ -0,0 +1,33 @@ +package es.princip.getp.infra.storage.domain; + +import es.princip.getp.domain.common.domain.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.nio.file.Path; +import java.nio.file.Paths; + +@Entity +@Table(name = "file_log") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FileLog extends BaseTimeEntity { + + @Id + @Getter + @Column(name = "file_log_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "file_name") + private String fileName; + + public FileLog(final String fileName) { + this.fileName = fileName; + } + + public Path getFilePath() { + return Paths.get(String.valueOf(id)).resolve(fileName); + } +} diff --git a/src/main/java/es/princip/getp/infra/storage/domain/FileLogRepository.java b/src/main/java/es/princip/getp/infra/storage/domain/FileLogRepository.java new file mode 100644 index 00000000..3ef0894b --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/domain/FileLogRepository.java @@ -0,0 +1,8 @@ +package es.princip.getp.infra.storage.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FileLogRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/exception/FailedFileSaveException.java b/src/main/java/es/princip/getp/infra/storage/exception/FailedFileSaveException.java new file mode 100644 index 00000000..d13ad94d --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/exception/FailedFileSaveException.java @@ -0,0 +1,14 @@ +package es.princip.getp.infra.storage.exception; + +import es.princip.getp.infra.exception.ErrorDescription; +import es.princip.getp.infra.exception.ExternalApiErrorException; + +public class FailedFileSaveException extends ExternalApiErrorException { + + private static final String code = "FAILED_FILE_SAVE"; + private static final String message = "파일 저장에 실패했습니다. 잠시 후 다시 시도해주세요."; + + public FailedFileSaveException() { + super(ErrorDescription.of(code, message)); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/exception/FailedImageSaveException.java b/src/main/java/es/princip/getp/infra/storage/exception/FailedImageSaveException.java new file mode 100644 index 00000000..0f93ee66 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/exception/FailedImageSaveException.java @@ -0,0 +1,14 @@ +package es.princip.getp.infra.storage.exception; + +import es.princip.getp.infra.exception.ErrorDescription; +import es.princip.getp.infra.exception.ExternalApiErrorException; + +public class FailedImageSaveException extends ExternalApiErrorException { + + private static final String code = "FAILED_IMAGE_SAVE"; + private static final String message = "사진 저장에 실패했습니다. 잠시 후 다시 시도해주세요."; + + public FailedImageSaveException() { + super(ErrorDescription.of(code, message)); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/exception/NotSupportedExtensionException.java b/src/main/java/es/princip/getp/infra/storage/exception/NotSupportedExtensionException.java new file mode 100644 index 00000000..7a957219 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/exception/NotSupportedExtensionException.java @@ -0,0 +1,14 @@ +package es.princip.getp.infra.storage.exception; + +import es.princip.getp.infra.exception.BusinessLogicException; +import es.princip.getp.infra.exception.ErrorDescription; + +public class NotSupportedExtensionException extends BusinessLogicException { + + private static final String code = "NOT_SUPPORTED_EXTENSION"; + private static final String message = "지원하지 않는 확장자입니다."; + + public NotSupportedExtensionException() { + super(ErrorDescription.of(code, message)); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/infra/LocalFileStorage.java b/src/main/java/es/princip/getp/infra/storage/infra/LocalFileStorage.java new file mode 100644 index 00000000..892549ca --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/infra/LocalFileStorage.java @@ -0,0 +1,59 @@ +package es.princip.getp.infra.storage.infra; + +import es.princip.getp.infra.storage.application.FileStorage; +import es.princip.getp.infra.storage.exception.FailedFileSaveException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +@Component +public class LocalFileStorage implements FileStorage { + + public LocalFileStorage( + @Value("${spring.storage.base-uri}") String baseUri, + @Value("${spring.storage.local.path}") String storagePath + ) { + this.baseUri = baseUri; + this.storagePath = Paths.get(storagePath).normalize().toAbsolutePath(); + this.fileStoragePath = Paths.get("files"); + } + + private final String baseUri; + private final Path storagePath; // 절대 경로 + private final Path fileStoragePath; // 상대 경로 + + @Override + public URI storeFile(final InputStream in, final Path destination) throws IOException { + final Path resolvedPath = resolvePath(destination); + makeDirectories(resolvedPath.getParent()); + Files.copy(in, resolvedPath, StandardCopyOption.REPLACE_EXISTING); + return createFileUri(resolvedPath); + } + + private Path resolvePath(final Path path) { + return storagePath.resolve(fileStoragePath).resolve(path); + } + + private void makeDirectories(final Path path) { + final File directory = new File(path.toUri()); + if (!directory.exists()) { + if (!directory.mkdirs()) { + throw new FailedFileSaveException(); + } + } + } + + private URI createFileUri(final Path path) { + final Path relativePath = storagePath.relativize(path); + final String fileUri = relativePath.toString().replace("\\", "/"); + return URI.create(baseUri + fileUri); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/presentation/FileStorageController.java b/src/main/java/es/princip/getp/infra/storage/presentation/FileStorageController.java new file mode 100644 index 00000000..cd048757 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/presentation/FileStorageController.java @@ -0,0 +1,36 @@ +package es.princip.getp.infra.storage.presentation; + +import es.princip.getp.infra.dto.response.ApiResponse; +import es.princip.getp.infra.storage.application.FileUploadService; +import es.princip.getp.infra.storage.presentation.dto.FileUploadResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URI; + +import static es.princip.getp.infra.dto.response.ApiResponse.ApiSuccessResult; + +@RestController +@RequiredArgsConstructor +@RequestMapping("storage") +public class FileStorageController { + + private final FileUploadService fileUploadService; + + @PostMapping("/files") + @PreAuthorize("isAuthenticated()") + public ResponseEntity> uploadFile( + @RequestPart final MultipartFile file + ) { + final URI fileUri = fileUploadService.uploadFile(file); + final FileUploadResponse response = new FileUploadResponse(fileUri); + return ApiResponse.success(HttpStatus.CREATED, response); + } +} diff --git a/src/main/java/es/princip/getp/infra/storage/presentation/dto/FileUploadResponse.java b/src/main/java/es/princip/getp/infra/storage/presentation/dto/FileUploadResponse.java new file mode 100644 index 00000000..654facd1 --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/presentation/dto/FileUploadResponse.java @@ -0,0 +1,6 @@ +package es.princip.getp.infra.storage.presentation.dto; + +import java.net.URI; + +public record FileUploadResponse(URI fileUri) { +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5edf7c6e..7e4c697c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -5,7 +5,9 @@ server: spring: storage: - path: ${STORAGE_PATH} + local: + path: ${STORAGE_PATH} + base-uri: ${STORAGE_BASE_URI} datasource: url: ${DB_URL} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index ad70fa69..6e429f1d 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -5,7 +5,9 @@ server: spring: storage: - path: ${STORAGE_PATH} + local: + path: ${STORAGE_PATH} + base-uri: ${STORAGE_BASE_URI} datasource: url: ${DB_URL} diff --git a/test/storage/images/image.jpeg b/test/storage/images/image.jpeg deleted file mode 100644 index c5d85b9de5725f4d4c78579b437c88b5ec57d763..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95987 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τ|LFycvYP&k9bj#7P6R*;;htLp%BUq=ka%=vCj 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 B9R&aY From 6c660791c32dabe9673bb2067fabda4633107e2e Mon Sep 17 00:00:00 2001 From: Changyu Shin Date: Wed, 7 Aug 2024 18:34:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?GETP-170=20test:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/member/upload-profile-image.adoc | 3 +- src/docs/asciidoc/storage/upload-file.adoc | 1 + .../storage/fixture/FileStorageFixture.java | 6 ++ .../storage/fixture/ImageStorageFixture.java | 7 +- .../infra/storage/fixture/StorageFixture.java | 15 +++++ .../storage/infra/LocalFileStorageTest.java | 33 +++++++++ .../FileStorageControllerTest.java | 67 +++++++++++++++++++ .../infra/support/AbstractControllerTest.java | 7 ++ src/test/resources/application-test.yml | 5 ++ 9 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 src/docs/asciidoc/storage/upload-file.adoc create mode 100644 src/test/java/es/princip/getp/infra/storage/fixture/FileStorageFixture.java create mode 100644 src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java create mode 100644 src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java create mode 100644 src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java diff --git a/src/docs/asciidoc/member/upload-profile-image.adoc b/src/docs/asciidoc/member/upload-profile-image.adoc index a6fb1e2a..d6b691a8 100644 --- a/src/docs/asciidoc/member/upload-profile-image.adoc +++ b/src/docs/asciidoc/member/upload-profile-image.adoc @@ -1,2 +1 @@ -operation::/upload-profile-image/upload-profile-image/[snippets="httpie-request,request-parts,http-response,response-fields-data"] -operation::/upload-profile-image/upload-profile-image-error-code/[snippets="error-code-fields"] \ No newline at end of file +operation::/upload-profile-image/upload-profile-image/[snippets="httpie-request,request-headers,request-parts,http-response,response-fields-data"] \ No newline at end of file diff --git a/src/docs/asciidoc/storage/upload-file.adoc b/src/docs/asciidoc/storage/upload-file.adoc new file mode 100644 index 00000000..f680540d --- /dev/null +++ b/src/docs/asciidoc/storage/upload-file.adoc @@ -0,0 +1 @@ +operation::/upload-file/upload-file/[snippets="httpie-request,request-headers,request-parts,http-response,response-fields-data"] \ No newline at end of file diff --git a/src/test/java/es/princip/getp/infra/storage/fixture/FileStorageFixture.java b/src/test/java/es/princip/getp/infra/storage/fixture/FileStorageFixture.java new file mode 100644 index 00000000..3fb4905f --- /dev/null +++ b/src/test/java/es/princip/getp/infra/storage/fixture/FileStorageFixture.java @@ -0,0 +1,6 @@ +package es.princip.getp.infra.storage.fixture; + +public class FileStorageFixture { + + public static final String DUMMY_TEXT = "테스트"; +} diff --git a/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java b/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java index cc77fda9..dd452420 100644 --- a/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java +++ b/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java @@ -3,13 +3,10 @@ import org.springframework.mock.web.MockMultipartFile; import java.nio.file.Path; -import java.nio.file.Paths; -public class ImageStorageFixture { +import static es.princip.getp.infra.storage.fixture.StorageFixture.STORAGE_PATH; - public static final Path STORAGE_PATH = Paths.get("test/storage") - .normalize() - .toAbsolutePath(); +public class ImageStorageFixture { public static final Path IMAGE_STORAGE_PATH = STORAGE_PATH.resolve("images"); diff --git a/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java b/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java new file mode 100644 index 00000000..3e82ec08 --- /dev/null +++ b/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java @@ -0,0 +1,15 @@ +package es.princip.getp.infra.storage.fixture; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public class StorageFixture { + + public static final String BASE_URL = "https://storage.princip.es/"; + + public static final String STORAGE_PATH_STR = "src/test/resources/static/"; + + public static final Path STORAGE_PATH = Paths.get(STORAGE_PATH_STR) + .normalize() + .toAbsolutePath(); +} diff --git a/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java b/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java new file mode 100644 index 00000000..05aef0ca --- /dev/null +++ b/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java @@ -0,0 +1,33 @@ +package es.princip.getp.infra.storage.infra; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; + +import static es.princip.getp.infra.storage.fixture.FileStorageFixture.DUMMY_TEXT; +import static es.princip.getp.infra.storage.fixture.StorageFixture.BASE_URL; +import static es.princip.getp.infra.storage.fixture.StorageFixture.STORAGE_PATH_STR; +import static org.assertj.core.api.Assertions.assertThat; + +class LocalFileStorageTest { + + private final LocalFileStorage localFileStorage = new LocalFileStorage(BASE_URL, STORAGE_PATH_STR); + + @Test + void 파일을_로컬_스토리지에_저장한다() throws IOException { + final InputStream in = new ByteArrayInputStream(DUMMY_TEXT.getBytes()); + final String filePath = "1/test.txt"; + final Path destination = Path.of(filePath); + + final URI fileUri = localFileStorage.storeFile(in, destination); + + final File saved = new File(STORAGE_PATH_STR + "files/" + filePath); + assertThat(saved).exists(); + assertThat(fileUri).isEqualTo(URI.create(BASE_URL).resolve("files/" + filePath)); + } +} \ No newline at end of file diff --git a/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java b/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java new file mode 100644 index 00000000..356ae28b --- /dev/null +++ b/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java @@ -0,0 +1,67 @@ +package es.princip.getp.infra.storage.presentation; + +import es.princip.getp.infra.annotation.WithCustomMockUser; +import es.princip.getp.infra.storage.application.FileUploadService; +import es.princip.getp.infra.support.AbstractControllerTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.ResultActions; + +import java.net.URI; + +import static es.princip.getp.infra.storage.fixture.FileStorageFixture.DUMMY_TEXT; +import static es.princip.getp.infra.util.FieldDescriptorHelper.getDescriptor; +import static es.princip.getp.infra.util.HeaderDescriptorHelper.authorizationHeaderDescriptor; +import static es.princip.getp.infra.util.PayloadDocumentationHelper.responseFields; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(FileStorageController.class) +class FileStorageControllerTest extends AbstractControllerTest { + + @MockBean + private FileUploadService fileUploadService; + + @Nested + @DisplayName("파일 업로드") + class UploadFile { + + final MockMultipartFile file = new MockMultipartFile("file", DUMMY_TEXT.getBytes()); + + private ResultActions perform() throws Exception { + return mockMvc.perform(multipart("/storage/files") + .file(file) + .header("Authorization", "Bearer ${ACCESS_TOKEN}")); + } + + @Test + @WithCustomMockUser + @DisplayName("사용자는 파일을 업로드할 수 있다.") + void uploadFile() throws Exception { + given(fileUploadService.uploadFile(any())) + .willReturn(URI.create("https://storage.princip.es/files/1/test.txt")); + + perform() + .andExpect(status().isCreated()) + .andDo( + restDocs.document( + requestHeaders(authorizationHeaderDescriptor()), + requestParts(partWithName("file").description("업로드할 파일")), + responseFields( + getDescriptor("fileUri", "업로드된 파일 URI") + ) + ) + ) + .andDo(print()); + } + } +} \ No newline at end of file diff --git a/src/test/java/es/princip/getp/infra/support/AbstractControllerTest.java b/src/test/java/es/princip/getp/infra/support/AbstractControllerTest.java index 70218c16..90dbb180 100644 --- a/src/test/java/es/princip/getp/infra/support/AbstractControllerTest.java +++ b/src/test/java/es/princip/getp/infra/support/AbstractControllerTest.java @@ -20,6 +20,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -57,6 +58,12 @@ protected MockHttpServletRequestBuilder delete(final String uri, final Object... return contextPathAndContentType(RestDocumentationRequestBuilders.delete(contextPath + uri, values)); } + protected MockMultipartHttpServletRequestBuilder multipart(final String uri, final Object... values) { + return (MockMultipartHttpServletRequestBuilder) RestDocumentationRequestBuilders + .multipart(contextPath + uri, values) + .contextPath(contextPath); + } + protected static ResultMatcher errorCode(final ErrorCode errorCode) { return status().is(errorCode.status().value()); } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index c6e817fd..e1dd00a9 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -3,6 +3,11 @@ server: context-path: ${BASE_PATH} spring: + storage: + local: + path: ${STORAGE_PATH} + base-uri: ${STORAGE_BASE_URI} + datasource: url: ${DB_TEST_URL} driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver From 65de6fee46b5f85470e4c4b83954ed004e2e3c06 Mon Sep 17 00:00:00 2001 From: Changyu Shin Date: Wed, 7 Aug 2024 19:24:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?GETP-170=20feat:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=98=ED=99=98=20URI=EB=A5=BC=20=EC=A0=88?= =?UTF-8?q?=EB=8C=80=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/ProfileImageService.java | 55 ++++++++++++------ .../infra/ProfileImageServiceImpl.java | 46 --------------- .../handler/ApiErrorExceptionHandler.java | 5 +- .../storage/application/ImageStorage.java | 13 +++++ .../LocalImageStorage.java} | 56 ++++++++++--------- src/main/resources/application-dev.yml | 6 +- src/main/resources/application-local.yml | 7 ++- .../application/MemberServiceTest.java | 4 +- .../member/fixture/ProfileImageFixture.java | 9 ++- .../storage/fixture/ImageStorageFixture.java | 6 -- .../infra/storage/fixture/StorageFixture.java | 9 +-- .../storage/infra/LocalFileStorageTest.java | 6 +- .../FileStorageControllerTest.java | 5 +- 13 files changed, 108 insertions(+), 119 deletions(-) delete mode 100644 src/main/java/es/princip/getp/domain/member/command/infra/ProfileImageServiceImpl.java create mode 100644 src/main/java/es/princip/getp/infra/storage/application/ImageStorage.java rename src/main/java/es/princip/getp/infra/storage/{ImageStorage.java => infra/LocalImageStorage.java} (63%) diff --git a/src/main/java/es/princip/getp/domain/member/command/domain/service/ProfileImageService.java b/src/main/java/es/princip/getp/domain/member/command/domain/service/ProfileImageService.java index 56faafa0..897d69f7 100644 --- a/src/main/java/es/princip/getp/domain/member/command/domain/service/ProfileImageService.java +++ b/src/main/java/es/princip/getp/domain/member/command/domain/service/ProfileImageService.java @@ -3,24 +3,43 @@ import es.princip.getp.domain.member.command.domain.model.Member; import es.princip.getp.domain.member.command.domain.model.ProfileImage; import es.princip.getp.domain.member.command.exception.FailedToSaveProfileImageException; +import es.princip.getp.infra.storage.application.ImageStorage; +import es.princip.getp.infra.util.ImageUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -public interface ProfileImageService { - - /** - * 회원의 프로필 이미지를 저장한다. - * - * @param member 회원 - * @param image 프로필 이미지 MultiPartFile - * @throws FailedToSaveProfileImageException 프로필 이미지 저장에 실패한 경우 - * @return 저장된 프로필 이미지 - */ - ProfileImage saveProfileImage(Member member, MultipartFile image); - - /** - * 프로필 이미지를 삭제한다. - * - * @param profileImage 삭제할 프로필 이미지 - */ - void deleteProfileImage(ProfileImage profileImage); +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Service +@RequiredArgsConstructor +public class ProfileImageService { + + public static final String PROFILE_IMAGE_PREFIX = "profile"; + + private final ImageStorage imageStorage; + + public ProfileImage saveProfileImage(final Member member, final MultipartFile image) { + final Path destination = getPathToSaveProfileImage(member, image); + try (InputStream in = image.getInputStream()) { + final URI uri = imageStorage.storeImage(destination, in); + return ProfileImage.of(uri.toString()); + } catch (IOException exception) { + throw new FailedToSaveProfileImageException(); + } + } + + private Path getPathToSaveProfileImage(final Member member, final MultipartFile image) { + final String memberId = String.valueOf(member.getMemberId()); + final String fileName = ImageUtil.generateRandomFilename(image.getOriginalFilename()); + return Paths.get(memberId).resolve(PROFILE_IMAGE_PREFIX).resolve(fileName); + } + + public void deleteProfileImage(final ProfileImage profileImage) { + imageStorage.deleteImage(URI.create(profileImage.getUri())); + } } diff --git a/src/main/java/es/princip/getp/domain/member/command/infra/ProfileImageServiceImpl.java b/src/main/java/es/princip/getp/domain/member/command/infra/ProfileImageServiceImpl.java deleted file mode 100644 index ed1830a7..00000000 --- a/src/main/java/es/princip/getp/domain/member/command/infra/ProfileImageServiceImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package es.princip.getp.domain.member.command.infra; - -import es.princip.getp.domain.member.command.domain.model.Member; -import es.princip.getp.domain.member.command.domain.model.ProfileImage; -import es.princip.getp.domain.member.command.domain.service.ProfileImageService; -import es.princip.getp.domain.member.command.exception.FailedToSaveProfileImageException; -import es.princip.getp.infra.storage.ImageStorage; -import es.princip.getp.infra.util.ImageUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; -import java.nio.file.Paths; - -@Service -@RequiredArgsConstructor -public class ProfileImageServiceImpl implements ProfileImageService { - - public static final String PROFILE_IMAGE_PREFIX = "profile"; - - private final ImageStorage imageStorage; - - public ProfileImage saveProfileImage(final Member member, final MultipartFile image) { - final Path destination = getPathToSaveProfileImage(member, image); - try (InputStream in = image.getInputStream()) { - final String uri = imageStorage.storeImage(destination, in); - return ProfileImage.of(uri); - } catch (IOException exception) { - throw new FailedToSaveProfileImageException(); - } - } - - private Path getPathToSaveProfileImage(final Member member, final MultipartFile image) { - final String memberId = String.valueOf(member.getMemberId()); - final String fileName = ImageUtil.generateRandomFilename(image.getOriginalFilename()); - return Paths.get(memberId).resolve(PROFILE_IMAGE_PREFIX).resolve(fileName); - } - - public void deleteProfileImage(final ProfileImage profileImage) { - final Path path = Paths.get(profileImage.getUri()); - imageStorage.deleteImage(path); - } -} diff --git a/src/main/java/es/princip/getp/infra/exception/handler/ApiErrorExceptionHandler.java b/src/main/java/es/princip/getp/infra/exception/handler/ApiErrorExceptionHandler.java index b060a49e..52542556 100644 --- a/src/main/java/es/princip/getp/infra/exception/handler/ApiErrorExceptionHandler.java +++ b/src/main/java/es/princip/getp/infra/exception/handler/ApiErrorExceptionHandler.java @@ -3,17 +3,20 @@ import es.princip.getp.infra.dto.response.ApiErrorResponse; import es.princip.getp.infra.dto.response.ApiErrorResponse.ApiErrorResult; import es.princip.getp.infra.exception.ApiErrorException; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -@RestControllerAdvice +@Slf4j @Order(100) +@RestControllerAdvice public class ApiErrorExceptionHandler { @ExceptionHandler(ApiErrorException.class) public ResponseEntity handleBusinessLogicException(final ApiErrorException exception) { + log.debug("ApiErrorException: ", exception); return ApiErrorResponse.error(exception.getStatus(), exception.getDescription()); } } \ No newline at end of file diff --git a/src/main/java/es/princip/getp/infra/storage/application/ImageStorage.java b/src/main/java/es/princip/getp/infra/storage/application/ImageStorage.java new file mode 100644 index 00000000..b915af3c --- /dev/null +++ b/src/main/java/es/princip/getp/infra/storage/application/ImageStorage.java @@ -0,0 +1,13 @@ +package es.princip.getp.infra.storage.application; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; + +public interface ImageStorage { + + URI storeImage(Path destination, InputStream imageStream) throws IOException; + + void deleteImage(URI destination); +} diff --git a/src/main/java/es/princip/getp/infra/storage/ImageStorage.java b/src/main/java/es/princip/getp/infra/storage/infra/LocalImageStorage.java similarity index 63% rename from src/main/java/es/princip/getp/infra/storage/ImageStorage.java rename to src/main/java/es/princip/getp/infra/storage/infra/LocalImageStorage.java index 975ee163..d358d4fd 100644 --- a/src/main/java/es/princip/getp/infra/storage/ImageStorage.java +++ b/src/main/java/es/princip/getp/infra/storage/infra/LocalImageStorage.java @@ -1,5 +1,6 @@ -package es.princip.getp.infra.storage; +package es.princip.getp.infra.storage.infra; +import es.princip.getp.infra.storage.application.ImageStorage; import es.princip.getp.infra.storage.exception.FailedImageSaveException; import es.princip.getp.infra.util.ImageUtil; import lombok.Getter; @@ -10,6 +11,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -18,13 +20,21 @@ @Component @Slf4j @Getter -public class ImageStorage { +public class LocalImageStorage implements ImageStorage { - public ImageStorage(@Value("${spring.storage.local.path}") String storagePath) { + public LocalImageStorage( + @Value("${server.servlet.context-path}") String contextPath, + @Value("${spring.storage.base-uri}") String baseUri, + @Value("${spring.storage.local.path}") String storagePath + ) { + this.contextPath = contextPath; + this.baseUri = baseUri; this.storagePath = Paths.get(storagePath).normalize().toAbsolutePath(); this.imageStoragePath = Paths.get("images"); } + private final String contextPath; + private final String baseUri; private final Path storagePath; // 절대 경로 private final Path imageStoragePath; // 상대 경로 @@ -35,10 +45,13 @@ public ImageStorage(@Value("${spring.storage.local.path}") String storagePath) { * @param imageStream 사진의 InputStream * @return imageStoragePath부터 시작하는 URI */ - public String storeImage(Path destination, InputStream imageStream) { + @Override + public URI storeImage(Path destination, InputStream imageStream) throws IOException { validateImage(imageStream); - copyImageToDestination(imageStream, resolvePath(destination)); - return "/" + storagePath.relativize(destination).toUri(); + final Path resolvedPath = resolvePath(destination); + makeDirectories(resolvedPath.getParent()); + Files.copy(imageStream, resolvedPath, StandardCopyOption.REPLACE_EXISTING); + return createFileUri(resolvedPath); } /** @@ -46,12 +59,14 @@ public String storeImage(Path destination, InputStream imageStream) { * * @param destination 삭제할 사진 경로 */ - public void deleteImage(Path destination) { + @Override + public void deleteImage(URI destination) { + final Path path = Paths.get(destination.getPath().replace(contextPath, "")); try { - if (destination.startsWith(getAbsoluteImageStoragePath())) { - Files.delete(destination); + if (path.startsWith(getAbsoluteImageStoragePath())) { + Files.delete(path); } else { - Files.delete(this.storagePath.resolve(destination)); + Files.delete(this.storagePath.resolve(path)); } } catch (IOException exception) { throw new FailedImageSaveException(); @@ -89,21 +104,6 @@ private Path resolvePath(Path path) { .resolve(path); } - /** - * 이미지를 destination에 복사합니다. - * - * @param imageStream 이미지의 InputStream - * @param destination 이미지를 저장할 경로 - */ - private void copyImageToDestination(InputStream imageStream, Path destination) { - try { - makeDirectories(destination.getParent()); - Files.copy(imageStream, destination, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException exception) { - throw new FailedImageSaveException(); - } - } - /** * path에 디렉토리를 생성합니다. * @@ -117,4 +117,10 @@ private void makeDirectories(Path path) { } } } + + private URI createFileUri(final Path path) { + final Path relativePath = storagePath.relativize(path); + final String fileUri = relativePath.toString().replace("\\", "/"); + return URI.create(baseUri + fileUri); + } } \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 7e4c697c..0b487e05 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -19,8 +19,10 @@ spring: ddl-auto: update properties: hibernate: - show-sql: true + show_sql: true format_sql: true + highlight_sql: true + use_sql_comments: true jdbc: time_zone: Asia/Seoul default_batch_fetch_size: 20 @@ -67,8 +69,6 @@ logging: org: springframework: security: DEBUG - hibernate: - SQL: DEBUG type: descriptor: sql: diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 6e429f1d..1dd8ad6c 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -19,8 +19,10 @@ spring: ddl-auto: update properties: hibernate: - show-sql: true + show_sql: true format_sql: true + highlight_sql: true + use_sql_comments: true jdbc: time_zone: Asia/Seoul default_batch_fetch_size: 20 @@ -68,11 +70,10 @@ spring: logging: level: + es.princip.getp: DEBUG org: springframework: security: DEBUG - hibernate: - SQL: DEBUG type: descriptor: sql: diff --git a/src/test/java/es/princip/getp/domain/member/command/application/MemberServiceTest.java b/src/test/java/es/princip/getp/domain/member/command/application/MemberServiceTest.java index 2020ef02..d47d2df6 100644 --- a/src/test/java/es/princip/getp/domain/member/command/application/MemberServiceTest.java +++ b/src/test/java/es/princip/getp/domain/member/command/application/MemberServiceTest.java @@ -6,8 +6,8 @@ import es.princip.getp.domain.member.command.domain.model.MemberRepository; import es.princip.getp.domain.member.command.domain.model.MemberType; import es.princip.getp.domain.member.command.domain.model.ProfileImage; +import es.princip.getp.domain.member.command.domain.service.ProfileImageService; import es.princip.getp.domain.member.command.domain.service.ServiceTermAgreementService; -import es.princip.getp.domain.member.command.infra.ProfileImageServiceImpl; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -45,7 +45,7 @@ class MemberServiceTest { private ServiceTermAgreementService agreementService; @Mock - private ProfileImageServiceImpl profileImageService; + private ProfileImageService profileImageService; @InjectMocks private MemberService memberService; diff --git a/src/test/java/es/princip/getp/domain/member/fixture/ProfileImageFixture.java b/src/test/java/es/princip/getp/domain/member/fixture/ProfileImageFixture.java index 41b0cb72..15392c60 100644 --- a/src/test/java/es/princip/getp/domain/member/fixture/ProfileImageFixture.java +++ b/src/test/java/es/princip/getp/domain/member/fixture/ProfileImageFixture.java @@ -2,12 +2,17 @@ import es.princip.getp.domain.member.command.domain.model.ProfileImage; +import java.net.URI; + +import static es.princip.getp.infra.storage.fixture.StorageFixture.BASE_URI; + public class ProfileImageFixture { private static final String FILE_NAME = "image.jpg"; public static ProfileImage profileImage(final Long memberId) { - final String profileImage = String.format("/images/%d/profile/%s", memberId, FILE_NAME); - return ProfileImage.of(profileImage); + final String profileImageUri = String.format("/images/%d/profile/%s", memberId, FILE_NAME); + final URI uri = URI.create(BASE_URI).resolve(profileImageUri); + return ProfileImage.of(uri.toString()); } } diff --git a/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java b/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java index dd452420..57629a19 100644 --- a/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java +++ b/src/test/java/es/princip/getp/infra/storage/fixture/ImageStorageFixture.java @@ -2,14 +2,8 @@ import org.springframework.mock.web.MockMultipartFile; -import java.nio.file.Path; - -import static es.princip.getp.infra.storage.fixture.StorageFixture.STORAGE_PATH; - public class ImageStorageFixture { - public static final Path IMAGE_STORAGE_PATH = STORAGE_PATH.resolve("images"); - public static MockMultipartFile imageMultiPartFile() { return new MockMultipartFile( "image", diff --git a/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java b/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java index 3e82ec08..43ba2cce 100644 --- a/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java +++ b/src/test/java/es/princip/getp/infra/storage/fixture/StorageFixture.java @@ -1,15 +1,8 @@ package es.princip.getp.infra.storage.fixture; -import java.nio.file.Path; -import java.nio.file.Paths; - public class StorageFixture { - public static final String BASE_URL = "https://storage.princip.es/"; + public static final String BASE_URI = "https://storage.princip.es/"; public static final String STORAGE_PATH_STR = "src/test/resources/static/"; - - public static final Path STORAGE_PATH = Paths.get(STORAGE_PATH_STR) - .normalize() - .toAbsolutePath(); } diff --git a/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java b/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java index 05aef0ca..ab192aa3 100644 --- a/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java +++ b/src/test/java/es/princip/getp/infra/storage/infra/LocalFileStorageTest.java @@ -10,13 +10,13 @@ import java.nio.file.Path; import static es.princip.getp.infra.storage.fixture.FileStorageFixture.DUMMY_TEXT; -import static es.princip.getp.infra.storage.fixture.StorageFixture.BASE_URL; +import static es.princip.getp.infra.storage.fixture.StorageFixture.BASE_URI; import static es.princip.getp.infra.storage.fixture.StorageFixture.STORAGE_PATH_STR; import static org.assertj.core.api.Assertions.assertThat; class LocalFileStorageTest { - private final LocalFileStorage localFileStorage = new LocalFileStorage(BASE_URL, STORAGE_PATH_STR); + private final LocalFileStorage localFileStorage = new LocalFileStorage(BASE_URI, STORAGE_PATH_STR); @Test void 파일을_로컬_스토리지에_저장한다() throws IOException { @@ -28,6 +28,6 @@ class LocalFileStorageTest { final File saved = new File(STORAGE_PATH_STR + "files/" + filePath); assertThat(saved).exists(); - assertThat(fileUri).isEqualTo(URI.create(BASE_URL).resolve("files/" + filePath)); + assertThat(fileUri).isEqualTo(URI.create(BASE_URI).resolve("files/" + filePath)); } } \ No newline at end of file diff --git a/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java b/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java index 356ae28b..8da3b1b7 100644 --- a/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java +++ b/src/test/java/es/princip/getp/infra/storage/presentation/FileStorageControllerTest.java @@ -10,6 +10,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.multipart.MultipartFile; import java.net.URI; @@ -47,8 +48,8 @@ private ResultActions perform() throws Exception { @WithCustomMockUser @DisplayName("사용자는 파일을 업로드할 수 있다.") void uploadFile() throws Exception { - given(fileUploadService.uploadFile(any())) - .willReturn(URI.create("https://storage.princip.es/files/1/test.txt")); + given(fileUploadService.uploadFile(any(MultipartFile.class))) + .willReturn(URI.create("https://storage.princip.es/files/1/test.pdf")); perform() .andExpect(status().isCreated())