Skip to content

Commit

Permalink
[BE] S3 를 통한 Image Upload 기능 구현 (#428)
Browse files Browse the repository at this point in the history
* refactor : s3 패키지 추가로 인한 에러 Code 수정

* feat : s3 exception 추가

* refactor : image extension 추출 방식 수정

* refactor : S3Client 가 IOException 을 throw 할 수 있도록 작성

* style : 프린트, 주석 제거

* test : imageExtension Test 작성

* refactor : image 가 요청으로 들어오지 않는 경우를 고려해 로직 수정

* test : 이미지가 null 로 들어오는 경우 test 작성

* feat : 병합시에도 S3 Image Upload 가 가능하도록 구현

* refactor : 기본 이미지 URL 변경

* refactor : 기본 이미지의 처리를 TopicInfo -> Image 에서 할 수 있도록 수정

* refactor : 주석 앞에 TODO 추가

* refactor : fromImageFileName -> from 으로 메서드 명 변경

* refactor : getExtension -> findExtension 으로 변경

* refactor : S3 관련 Service 네이밍 수정

* [BE] Fix/#426 Token CORS 재설정 (#427)

* fix: RefreshToken Payload 추가 및 CORS 완화

* fix: Refresh Token Header 허용

* fix: CORS 재설정 및 sameSite None

* refactor : S3 관련 Service 네이밍 수정

* [BE] HotFix/#426 Refresh Token 중복 저장 방지 로직 수정 (#431)

* fix: RefreshToken Payload 추가 및 CORS 완화

* fix: Refresh Token Header 허용

* fix: CORS 재설정 및 sameSite None

* fix: refreshToken 존재 시 삭제 로직 변경

* [BE] HotFix/#426 delete 메서드에 clearAutomatically 속성 적용 (#432)

* fix: RefreshToken Payload 추가 및 CORS 완화

* fix: Refresh Token Header 허용

* fix: CORS 재설정 및 sameSite None

* fix: refreshToken 존재 시 삭제 로직 변경

* fix: delete 메서드에 clearAutomatically 속성 적용

* [BE] HotFix/#426 tokenService flush 추가 (#433)

* fix: RefreshToken Payload 추가 및 CORS 완화

* fix: Refresh Token Header 허용

* fix: CORS 재설정 및 sameSite None

* fix: refreshToken 존재 시 삭제 로직 변경

* fix: delete 메서드에 clearAutomatically 속성 적용

* fix: delete 메서드에 clearAutomatically 속성 제거 및 flush 추가

* [BE] Refactor/#400 토픽 조회 시 업데이트 일시를 최근에 핀이 추가/변경된 일시로 변경 (#429)

* refactor: BaseEntity의 createdAt update 방지

* feat: Topic에 lastPinUpdatedAt 컬럼 추가, EntityListner 적용

- 기존 BaseEntity의 값들은 객체가 영속화될 때 저장된다.
- 이에 대해 일관성을 유지해야 한다. (핀 생성 일시, 핀 변경 일시 = 토픽의 최근 핀 변경 일시가 서로 같아야 하므로)
- 따라서 lastPinUpdatedAt 컬럼의 업데이트 또한 EntityListener 로 적용한다.

* feat: 토픽 조회 DTO의 updatedAt 값 lastPinUpdatedAt 으로 변경

* feat: 토픽 최신순 조회 로직 수정

- Topic에 lastPinUpdatedAt 추가로 인해 로직 수정 가능

* test: 토픽 조회 시 updatedAt 검증 테스트 추가

* chore: 로컬 테스트용 SQL에 테이블 컬럼 추가 변경 반영

* refactor: 토픽 Response Dto에 lastPinUpdatedAt 반영

* fix : 토큰 만료시간 및 redirect uri 수정

---------

Co-authored-by: jaeyeon kim <jakind@naver.com>

* [BE] Feature/#422 성능 측정을 위한 로깅 구현 (#434)

* feat: QueryCounter 객체 구현

* feat: QueryInspector 객체 구현

* feat: LatencyRecorder 객체 구현

* feat: LatencyLoggingFilter 객체 구현

* feat: LatencyRecorder Thread-safe 테스트 구현

* feat: HibernateConfig 구현

* test: 테스트 수정

* style: 개행 추가

* refactor: 수식 표현 방식 수정

* [BE] HotFix/#424 refresh token duplicated (#441)

* fix: RefreshToken Payload 추가 및 CORS 완화

* fix: Refresh Token Header 허용

* fix: CORS 재설정 및 sameSite None

* fix: 디버깅을 위한 에러코드 추가

* [BE] HOTFix/#424 validateTokensForReissue 디버깅을 위한 에러코드 추가 (#443)

* fix: RefreshToken Payload 추가 및 CORS 완화

* fix: Refresh Token Header 허용

* fix: CORS 재설정 및 sameSite None

* fix: 디버깅을 위한 에러코드 추가

* fix: validateTokensForReissue 디버깅을 위한 에러코드 추가

* fix: isExpired 임시 log 처리 (#444)

* Revert "fix: isExpired 임시 log 처리 (#444)"

This reverts commit 445f0dd.

* fix: cors Credentials 추가 (#458)

* [BE] Hotfix/cors allowHeaders 와일드카드 적용 (#462)

* fix: cors Credentials 추가

* fix: allowedHeaders 와일드카드 적용

* [BE] 부하테스트를 위한 Tomcat Log 추가 (#464)

* chore: yml 변수 적용 확인을 위한 debug 로그 추가

* chore: 톰캣 설정 기본값 추가

* chore: 톰캣 설정 기본값 추가

---------

Co-authored-by: yoondgu <doyoungwork@gmail.com>

* refactor : s3 패키지 추가로 인한 에러 Code 수정

* feat : s3 exception 추가

* refactor : image extension 추출 방식 수정

* refactor : S3Client 가 IOException 을 throw 할 수 있도록 작성

* style : 프린트, 주석 제거

* test : imageExtension Test 작성

* refactor : image 가 요청으로 들어오지 않는 경우를 고려해 로직 수정

* test : 이미지가 null 로 들어오는 경우 test 작성

* feat : 병합시에도 S3 Image Upload 가 가능하도록 구현

* refactor : 기본 이미지 URL 변경

* refactor : 기본 이미지의 처리를 TopicInfo -> Image 에서 할 수 있도록 수정

* refactor : 주석 앞에 TODO 추가

* refactor : fromImageFileName -> from 으로 메서드 명 변경

* refactor : getExtension -> findExtension 으로 변경

* refactor : S3 관련 Service 네이밍 수정

* refactor : S3 관련 Service 네이밍 수정

* refactor : topic, image errorCode 수정

* refactor : Exception 부분 네이밍 S3 -> Image 로 변경

* refactor : findExtension -> extractExtensio 으로 메서드 네이밍 변경

* refactor : 부정 조건문 제거

* refactor : Illegal Image File Extension 에러 메세지 수정

* refactor : action method consume type 순서 조정

---------

Co-authored-by: 준팍(junpak) <112045553+junpakPark@users.noreply.github.com>
Co-authored-by: Doy <doyoungwork@gmail.com>
Co-authored-by: zun <50602742+cpot5620@users.noreply.github.com>
  • Loading branch information
4 people authored Sep 21, 2023
1 parent ad7bcd4 commit 327e80a
Show file tree
Hide file tree
Showing 32 changed files with 476 additions and 203 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.mapbefine.mapbefine.s3.application;
package com.mapbefine.mapbefine.image.application;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public interface S3Service {
public interface ImageService {

String upload(MultipartFile multipartFile);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.mapbefine.mapbefine.s3.application;
package com.mapbefine.mapbefine.image.application;

import com.mapbefine.mapbefine.s3.domain.S3Client;
import com.mapbefine.mapbefine.s3.domain.UploadFile;
import com.mapbefine.mapbefine.image.domain.S3Client;
import com.mapbefine.mapbefine.image.domain.UploadFile;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
Expand All @@ -10,13 +10,13 @@

@Service
@Profile("!test")
public class S3ServiceImpl implements S3Service {
public class S3ImageService implements ImageService {

@Value("${prefix.upload.path}")
private String prefixUploadPath;
private final S3Client s3Client;

public S3ServiceImpl(S3Client s3Client) {
public S3ImageService(S3Client s3Client) {
this.s3Client = s3Client;
}

Expand All @@ -31,7 +31,7 @@ public String upload(MultipartFile multipartFile) {
}
}

private String getUploadPath(final UploadFile uploadFile) {
private String getUploadPath(UploadFile uploadFile) {
return String.join(
"/",
prefixUploadPath,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.mapbefine.mapbefine.image.domain;

import static com.mapbefine.mapbefine.image.exception.ImageErrorCode.ILLEGAL_IMAGE_FILE_EXTENSION;

import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException;
import java.util.Arrays;

public enum ImageExtension {

JPEG(".jpeg"),
JPG(".jpg"),
JFIF(".jfif"),
PNG(".png"),
SVG(".svg"),
;

private final String extension;

ImageExtension(final String extension) {
this.extension = extension;
}

public static ImageExtension from(String imageFileName) {
return Arrays.stream(values())
.filter(imageExtension -> imageFileName.endsWith(imageExtension.getExtension()))
.findFirst()
.orElseThrow(() -> new ImageBadRequestException(ILLEGAL_IMAGE_FILE_EXTENSION));
}

public String getExtension() {
return extension;
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.mapbefine.mapbefine.s3.domain;
package com.mapbefine.mapbefine.image.domain;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ImageName {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSS");
private static final String EXTENSION_DELIMITER = ".";

private final String fileName;

Expand All @@ -16,15 +15,14 @@ private ImageName(String fileName) {

public static ImageName from(String originalFileName) {
String fileName = FORMATTER.format(LocalDateTime.now());
String extension = getExtension(originalFileName);
String extension = extractExtension(originalFileName);

return new ImageName(fileName + extension);
}

private static String getExtension(String originalFileName) {
return originalFileName.substring(
originalFileName.lastIndexOf(EXTENSION_DELIMITER)
);
private static String extractExtension(String originalFileName) {
return ImageExtension.from(originalFileName)
.getExtension();
}

public String getFileName() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.mapbefine.mapbefine.s3.domain;
package com.mapbefine.mapbefine.image.domain;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.PutObjectRequest;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -20,27 +21,32 @@ public S3Client(AmazonS3 amazonS3) {
this.amazonS3 = amazonS3;
}

public void upload(MultipartFile multipartFile) {
public void upload(MultipartFile multipartFile) throws IOException {
File tempFile = null;

try {
tempFile = File.createTempFile("upload_", ".tmp");
multipartFile.transferTo(tempFile);
amazonS3.putObject(new PutObjectRequest(bucket, multipartFile.getOriginalFilename(), tempFile));
} catch (IOException e) { // TODO: 2023/09/07 Exception 을 수정
throw new RuntimeException(e);
amazonS3.putObject(new PutObjectRequest(
bucket,
multipartFile.getOriginalFilename(),
tempFile
));
} catch (IOException exception) {
throw new IOException(exception);
} finally {
removeTempFileIfExists(tempFile);
}
}

private void removeTempFileIfExists(final File tempFile) {
if (tempFile != null && tempFile.exists()) {
private void removeTempFileIfExists(File tempFile) {
if (Objects.nonNull(tempFile) && tempFile.exists()) {
tempFile.delete();
}
}

public void delete(String key) {
// TODO 현재는 일단 기능만 만들어놓고, API 는 만들어놓지 않았습니다 회의를 통해서 결정해야 할 사항이 있는 것 같아서요!
amazonS3.deleteObject(new DeleteObjectRequest(bucket, key));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mapbefine.mapbefine.s3.domain;
package com.mapbefine.mapbefine.image.domain;

import java.io.ByteArrayInputStream;
import java.io.File;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mapbefine.mapbefine.image.exception;

import lombok.Getter;

@Getter
public enum ImageErrorCode {

ILLEGAL_IMAGE_FILE_EXTENSION("10000", "지원하지 않는 이미지 파일입니다."),
IMAGE_FILE_IS_NULL("10001", "이미지가 선택되지 않았습니다.")
;

private final String code;
private final String message;

ImageErrorCode(String code, String message) {
this.code = code;
this.message = message;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mapbefine.mapbefine.image.exception;

import com.mapbefine.mapbefine.common.exception.BadRequestException;
import com.mapbefine.mapbefine.common.exception.ErrorCode;

public class ImageException {

public static class ImageBadRequestException extends BadRequestException {

public ImageBadRequestException(ImageErrorCode errorCode) {
super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage()));
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.FORBIDDEN_PIN_CREATE_OR_UPDATE;
import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_ID;
import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_IMAGE_ID;
import static com.mapbefine.mapbefine.image.exception.ImageErrorCode.IMAGE_FILE_IS_NULL;
import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_TOPIC_ID;

import com.mapbefine.mapbefine.auth.domain.AuthMember;
Expand All @@ -21,7 +22,8 @@
import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest;
import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException;
import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException;
import com.mapbefine.mapbefine.s3.application.S3Service;
import com.mapbefine.mapbefine.image.application.ImageService;
import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException;
Expand All @@ -43,22 +45,22 @@ public class PinCommandService {
private final TopicRepository topicRepository;
private final MemberRepository memberRepository;
private final PinImageRepository pinImageRepository;
private final S3Service s3Service;
private final ImageService imageService;

public PinCommandService(
PinRepository pinRepository,
LocationRepository locationRepository,
TopicRepository topicRepository,
MemberRepository memberRepository,
PinImageRepository pinImageRepository,
S3Service s3Service
ImageService imageService
) {
this.pinRepository = pinRepository;
this.locationRepository = locationRepository;
this.topicRepository = topicRepository;
this.memberRepository = memberRepository;
this.pinImageRepository = pinImageRepository;
this.s3Service = s3Service;
this.imageService = imageService;
}

public long save(
Expand All @@ -78,12 +80,21 @@ public long save(
member
);

images.forEach(image -> addImageToPin(image, pin));
addPinImagesToPin(images, pin);

pinRepository.save(pin);

return pin.getId();
}

private void addPinImagesToPin(final List<MultipartFile> images, final Pin pin) {
if (Objects.isNull(images)) {
return;
}

images.forEach(image -> addImageToPin(image, pin));
}

private Topic findTopic(Long topicId) {
if (Objects.isNull(topicId)) {
throw new TopicBadRequestException(ILLEGAL_TOPIC_ID);
Expand Down Expand Up @@ -153,7 +164,11 @@ public void addImage(AuthMember authMember, PinImageCreateRequest request) {
}

private void addImageToPin(MultipartFile image, Pin pin) {
String imageUrl = s3Service.upload(image);
if (Objects.isNull(image)) {
throw new ImageBadRequestException(IMAGE_FILE_IS_NULL);
}

String imageUrl = imageService.upload(image);
PinImage.createPinImageAssociatedWithPin(imageUrl, pin);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ public PinController(PinCommandService pinCommandService, PinQueryService pinQue
}

@LoginRequired
@PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<Void> add(
AuthMember member,
@RequestPart List<MultipartFile> images,
@RequestPart(required = false) List<MultipartFile> images,
@RequestPart PinCreateRequest request
) {
long savedId = pinCommandService.save(member, images, request);
Expand Down Expand Up @@ -101,12 +101,12 @@ public ResponseEntity<List<PinResponse>> findAllPinsByMemberId(
@LoginRequired
@PostMapping(
value = "/images",
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}
)
public ResponseEntity<Void> addImage(
AuthMember member,
@RequestPart Long pinId,
@RequestPart MultipartFile image
@RequestPart(required = false) MultipartFile image
) {
pinCommandService.addImage(member, new PinImageCreateRequest(pinId, image));

Expand Down
Loading

0 comments on commit 327e80a

Please sign in to comment.