Skip to content

Commit

Permalink
GETP-170 feat: 프로필 이미지 업로드 반환 URI를 절대 경로로 변경
Browse files Browse the repository at this point in the history
  • Loading branch information
scv1702 committed Aug 7, 2024
1 parent 6c66079 commit 65de6fe
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiErrorResult> handleBusinessLogicException(final ApiErrorException exception) {
log.debug("ApiErrorException: ", exception);
return ApiErrorResponse.error(exception.getStatus(), exception.getDescription());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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; // 상대 경로

Expand All @@ -35,23 +45,28 @@ 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);
}

/**
* 주어진 경로의 사진을 삭제합니다.
*
* @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();
Expand Down Expand Up @@ -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에 디렉토리를 생성합니다.
*
Expand All @@ -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);
}
}
6 changes: 3 additions & 3 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,8 +69,6 @@ logging:
org:
springframework:
security: DEBUG
hibernate:
SQL: DEBUG
type:
descriptor:
sql:
Expand Down
7 changes: 4 additions & 3 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,11 +70,10 @@ spring:

logging:
level:
es.princip.getp: DEBUG
org:
springframework:
security: DEBUG
hibernate:
SQL: DEBUG
type:
descriptor:
sql:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,7 +45,7 @@ class MemberServiceTest {
private ServiceTermAgreementService agreementService;

@Mock
private ProfileImageServiceImpl profileImageService;
private ProfileImageService profileImageService;

@InjectMocks
private MemberService memberService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
}
}
Loading

0 comments on commit 65de6fe

Please sign in to comment.