Skip to content

Commit

Permalink
Merge pull request #17 from Team-Lecue/feat/#7-image_upload_s3_presig…
Browse files Browse the repository at this point in the history
…ned_url

[FEAT] 사진 업로드를 위한 S3 + Presigned URL 구현
  • Loading branch information
ddongseop authored Jan 3, 2024
2 parents a5458ae + bce425b commit c9e0282
Show file tree
Hide file tree
Showing 21 changed files with 565 additions and 111 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ dependencies {
// implementation 'org.springframework.boot:spring-boot-starter-security'
// testImplementation 'org.springframework.security:spring-security-test'

// Test
// Test & Validation
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('bootBuildImage') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ public class RollingPaper extends BaseTimeEntity {
private User user;

@OneToMany(mappedBy = "rollingPaper")
private List<Postit> postits = new ArrayList<>();
private final List<Postit> postits = new ArrayList<>();

public void addPostit(Postit postit) {
postits.add(postit);
}

@OneToMany(mappedBy = "rollingPaper")
private List<PostedSticker> postedStickers = new ArrayList<>();
private final List<PostedSticker> postedStickers = new ArrayList<>();

public void addPostedSticker(PostedSticker postedSticker) {
postedStickers.add(postedSticker);
Expand All @@ -70,4 +70,19 @@ public RollingPaper(String uuid, String favoriteName, String favoriteImage, Stri
public static RollingPaper of(String uuid, String favoriteName, String favoriteImage, String title, String description, int backgroundColor, User user) {
return new RollingPaper(uuid, favoriteName, favoriteImage, title, description, backgroundColor, user);
}

// TODO S3 테스트용, 추후 삭제
public RollingPaper(String uuid, String favoriteName, String favoriteImage, String title, String description, int backgroundColor) {
this.uuid = uuid;
this.favoriteName = favoriteName;
this.favoriteImage = favoriteImage;
this.title = title;
this.description = description;
this.backgroundColor = backgroundColor;
}

// TODO S3 테스트용, 추후 삭제
public static RollingPaper test(String favoriteImage, String title) {
return new RollingPaper("test", "test", favoriteImage, title, "test", 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ public class User extends BaseTimeEntity {
private String nickname;

@OneToMany(mappedBy = "user")
private List<RollingPaper> rollingPapers = new ArrayList<>();
private final List<RollingPaper> rollingPapers = new ArrayList<>();

public void addRollingPaper(RollingPaper rollingPaper) {
rollingPapers.add(rollingPaper);
}

@OneToMany(mappedBy = "user")
private List<Postit> postits = new ArrayList<>();
private final List<Postit> postits = new ArrayList<>();

public void addPostit(Postit postit) {
postits.add(postit);
}

@OneToMany(mappedBy = "user")
private List<PostedSticker> postedStickers = new ArrayList<>();
private final List<PostedSticker> postedStickers = new ArrayList<>();

public void addPostedSticker(PostedSticker postedSticker) {
postedStickers.add(postedSticker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.sopt.lequuServer.global.common.exception.enums.ErrorType;
import org.sopt.lequuServer.global.common.exception.enums.SuccessType;
import org.sopt.lequuServer.global.exception.enums.ErrorType;
import org.sopt.lequuServer.global.exception.enums.SuccessType;

@Getter
@JsonPropertyOrder({"code", "message", "data"})
Expand Down Expand Up @@ -37,6 +37,10 @@ public static ApiResponse<?> error(ErrorType errorType, String message) {
return new ApiResponse<>(errorType.getHttpStatusCode(), message);
}

public static <T> ApiResponse<T> error(ErrorType errorType, T data) {
return new ApiResponse<>(errorType.getHttpStatusCode(), errorType.getMessage(), data);
}

public static <T> ApiResponse<T> error(ErrorType errorType, String message, T data) {
return new ApiResponse<>(errorType.getHttpStatusCode(), message, data);
}
Expand All @@ -45,7 +49,7 @@ public static <T> ApiResponse<Exception> error(ErrorType errorType, Exception e)
return new ApiResponse<>(errorType.getHttpStatusCode(), errorType.getMessage(), e);
}

public static <T> ApiResponse<T> error(ErrorType errorType, T data) {
return new ApiResponse<>(errorType.getHttpStatusCode(), errorType.getMessage(), data);
public static <T> ApiResponse<Exception> error(ErrorType errorType, String message, Exception e) {
return new ApiResponse<>(errorType.getHttpStatusCode(), message, e);
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class AWSConfig {
Expand Down Expand Up @@ -45,4 +46,12 @@ public S3Client getS3Client() {
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}

@Bean
public S3Presigner getS3Presigner() {
return S3Presigner.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.sopt.lequuServer.global.exception;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sopt.lequuServer.global.common.dto.ApiResponse;
import org.sopt.lequuServer.global.exception.enums.ErrorType;
import org.sopt.lequuServer.global.exception.model.CustomException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import jakarta.validation.UnexpectedTypeException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static org.sopt.lequuServer.global.exception.enums.ErrorType.*;

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {


/**
* 400 VALIDATION_ERROR
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ApiResponse<?> handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) {

Errors errors = e.getBindingResult();
Map<String, String> validateDetails = new HashMap<>();

for (FieldError error : errors.getFieldErrors()) {
String validKeyName = String.format("valid_%s", error.getField());
validateDetails.put(validKeyName, error.getDefaultMessage());
}
return ApiResponse.error(REQUEST_VALIDATION_ERROR, validateDetails);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(UnexpectedTypeException.class)
protected ApiResponse<?> handleUnexprectedTypeException(final UnexpectedTypeException e) {
return ApiResponse.error(INVALID_TYPE_ERROR);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ApiResponse<?> handlerMethodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException e) {
return ApiResponse.error(INVALID_TYPE_ERROR);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingRequestHeaderException.class)
protected ApiResponse<?> handlerMissingRequestHeaderException(final MissingRequestHeaderException e) {
return ApiResponse.error(INVALID_MISSING_HEADER_ERROR);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
protected ApiResponse<?> handlerHttpMessageNotReadableException(final HttpMessageNotReadableException e) {
return ApiResponse.error(INVALID_HTTP_REQUEST_ERROR);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ApiResponse<?> handlerHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e) {
return ApiResponse.error(INVALID_HTTP_METHOD_ERROR);
}


/**
* 500 INTERNEL_SERVER_ERROR
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
protected ApiResponse<Exception> handleException(final Exception e, final HttpServletRequest request) {

log.error("500 Error Occured: {}", e.getMessage(), e);

return ApiResponse.error(INTERNAL_SERVER_ERROR, e);
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IllegalArgumentException.class)
public ApiResponse<Exception> handlerIllegalArgumentException(final IllegalArgumentException e, final HttpServletRequest request) {
return ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR, e);
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IOException.class)
public ApiResponse<Exception> handlerIOException(final IOException e, final HttpServletRequest request) {
return ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR, e);
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(RuntimeException.class)
public ApiResponse<Exception> handlerRuntimeException(final RuntimeException e, final HttpServletRequest request) {

if (e.getMessage() != null) {
return ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR, e.getMessage(), e);
} else {
return ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR, e);
}
}


/**
* CUSTOM_ERROR
*/
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ApiResponse<?>> handleCustomException(CustomException e) {

log.error("CustomException Occured: {}", e.getMessage(), e);

return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getErrorType(), e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.sopt.lequuServer.global.exception.enums;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ErrorType {

/**
* 400 BAD REQUEST
*/
REQUEST_VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
INVALID_TYPE_ERROR(HttpStatus.BAD_REQUEST, "잘못된 타입이 입력되었습니다."),
INVALID_MISSING_HEADER_ERROR(HttpStatus.BAD_REQUEST, "요청에 필요한 헤더값이 존재하지 않습니다."),
INVALID_HTTP_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "허용되지 않는 문자열이 입력되었습니다."),
INVALID_HTTP_METHOD_ERROR(HttpStatus.BAD_REQUEST, "지원되지 않는 HTTP method 요청입니다."),
IMAGE_EXTENSION_ERROR(HttpStatus.BAD_REQUEST, "이미지 확장자는 jpg, png, webp만 가능합니다."),
IMAGE_SIZE_ERROR(HttpStatus.BAD_REQUEST, "이미지 사이즈는 5MB를 넘을 수 없습니다."),

/**
* 500 INTERNAL SERVER ERROR
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다."),
GET_UPLOAD_PRESIGNED_URL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "업로드를 위한 Presigned URL 획득에 실패했습니다.")
;

private final HttpStatus httpStatus;
private final String message;

public int getHttpStatusCode() {
return httpStatus.value();
}
}
Loading

0 comments on commit c9e0282

Please sign in to comment.