diff --git a/build.gradle b/build.gradle index 0148f54..85787cb 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + + // AWS + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java b/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java new file mode 100644 index 0000000..dc3a7a0 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java @@ -0,0 +1,40 @@ +package fairytale.tbd.global.aws.s3; + +import java.io.IOException; + +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; + +import fairytale.tbd.global.config.AmazonConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AmazonS3Manager { + + private final AmazonS3 amazonS3; + private final AmazonConfig amazonConfig; + + public String uploadFile(String keyName, MultipartFile file) { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); + try { + PutObjectResult putObjectResult = amazonS3.putObject( + new PutObjectRequest(amazonConfig.getBucket(), keyName, file.getInputStream(), metadata)); + log.info("result={}", putObjectResult.getContentMd5()); + } catch (IOException e) { + log.error("error at AmazonS3Manager uploadFile : {}", (Object)e.getStackTrace()); + } + + return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString(); + } + +} diff --git a/src/main/java/fairytale/tbd/global/config/AmazonConfig.java b/src/main/java/fairytale/tbd/global/config/AmazonConfig.java new file mode 100644 index 0000000..acb5591 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/config/AmazonConfig.java @@ -0,0 +1,56 @@ +package fairytale.tbd.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; + +@Configuration +@Getter +public class AmazonConfig { + + private AWSCredentials awsCredentials; + + @Value("${cloud.aws.s3.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.s3.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.s3.region.static}") + private String region; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + + + @PostConstruct + public void init(){ + this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + } + + @Bean + public AmazonS3 amazonS3(){ + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + @Bean + public AWSCredentialsProvider awsCredentialsProvider(){ + return new AWSStaticCredentialsProvider(awsCredentials); + } + +} diff --git a/src/main/java/fairytale/tbd/global/entity/BaseEntity.java b/src/main/java/fairytale/tbd/global/entity/BaseEntity.java new file mode 100644 index 0000000..6c2a7a4 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/entity/BaseEntity.java @@ -0,0 +1,22 @@ +package fairytale.tbd.global.entity; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/fairytale/tbd/global/enums/statuscode/BaseCode.java b/src/main/java/fairytale/tbd/global/enums/statuscode/BaseCode.java new file mode 100644 index 0000000..f4b4c43 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/enums/statuscode/BaseCode.java @@ -0,0 +1,13 @@ +package fairytale.tbd.global.enums.statuscode; + +import org.springframework.http.HttpStatus; + +public interface BaseCode { + String getCode(); + + String getMessage(); + + HttpStatus getHttpStatus(); + + Integer getStatusValue(); +} diff --git a/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java b/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java new file mode 100644 index 0000000..5c3397b --- /dev/null +++ b/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java @@ -0,0 +1,46 @@ +package fairytale.tbd.global.enums.statuscode; + +import org.springframework.http.HttpStatus; + +public enum ErrorStatus implements BaseCode{ + // common + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), + _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + + + // ElevenLabs + _FILE_CONVERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FILE5001", "파일 변환에 실패했습니다."); + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + ErrorStatus(HttpStatus httpStatus, String code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public Integer getStatusValue() { + return httpStatus.value(); + } +} diff --git a/src/main/java/fairytale/tbd/global/enums/statuscode/SuccessStatus.java b/src/main/java/fairytale/tbd/global/enums/statuscode/SuccessStatus.java new file mode 100644 index 0000000..48fd35a --- /dev/null +++ b/src/main/java/fairytale/tbd/global/enums/statuscode/SuccessStatus.java @@ -0,0 +1,38 @@ +package fairytale.tbd.global.enums.statuscode; + +import org.springframework.http.HttpStatus; + +public enum SuccessStatus implements BaseCode{ + _OK(HttpStatus.OK, "COMMON200", "성공입니다."), + _ACCEPTED(HttpStatus.ACCEPTED, "COMMON204", "별도의 응답 데이터가 없으며, 정상 처리되었습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + SuccessStatus(HttpStatus httpStatus, String code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public Integer getStatusValue() { + return httpStatus.value(); + } +} diff --git a/src/main/java/fairytale/tbd/global/exception/ExceptionAdvice.java b/src/main/java/fairytale/tbd/global/exception/ExceptionAdvice.java new file mode 100644 index 0000000..ff7f749 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/exception/ExceptionAdvice.java @@ -0,0 +1,84 @@ +package fairytale.tbd.global.exception; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import fairytale.tbd.global.enums.statuscode.ErrorStatus; +import fairytale.tbd.global.response.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class ExceptionAdvice extends ResponseEntityExceptionHandler { + @ExceptionHandler(value = GeneralException.class) + public ResponseEntity handleGeneralException(GeneralException exception, HttpServletRequest request) { + return handleExceptionInternal(exception, HttpHeaders.EMPTY, request); + } + + @Override + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException exception, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + + log.info("handleGeneralException 발생 ={}", exception); + Map errors = new LinkedHashMap<>(); + exception.getBindingResult().getFieldErrors().stream() + .forEach(fieldError -> { + + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, + (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + exception.getBindingResult().getGlobalErrors().stream() + .forEach(globalError -> { + log.info("globalError = {}", globalError); + String objectName = globalError.getObjectName(); + String errorMessage = Optional.ofNullable(globalError.getDefaultMessage()).orElse(""); + errors.merge(objectName, errorMessage, + (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(exception, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"), request, + errors); + } + + @ExceptionHandler + public ResponseEntity handlingException(Exception exception, WebRequest request) { + return handleExceptionInternal(exception, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, + ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, exception.getMessage()); + } + + private ResponseEntity handleExceptionInternal(GeneralException exception, HttpHeaders headers, + HttpServletRequest request) { + ApiResponse body = ApiResponse.onFailure(exception.getErrorCode(), exception.getErrorReason(), null); + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal(exception, body, headers, exception.getHttpStatus(), webRequest); + } + + private ResponseEntity handleExceptionInternal(Exception exception, ErrorStatus errorStatus, + HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { + ApiResponse body = ApiResponse.onFailure(errorStatus.getCode(), errorStatus.getMessage(), errorPoint); + return super.handleExceptionInternal(exception, body, headers, status, request); + } + + private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers, + ErrorStatus errorCommonStatus, WebRequest request, Map errorArgs) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), + errorArgs); + return super.handleExceptionInternal(e, body, headers, errorCommonStatus.getHttpStatus(), request); + } +} diff --git a/src/main/java/fairytale/tbd/global/exception/GeneralException.java b/src/main/java/fairytale/tbd/global/exception/GeneralException.java new file mode 100644 index 0000000..3dc4440 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/exception/GeneralException.java @@ -0,0 +1,23 @@ +package fairytale.tbd.global.exception; + +import org.springframework.http.HttpStatus; + +import fairytale.tbd.global.enums.statuscode.BaseCode; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class GeneralException extends RuntimeException{ + private final BaseCode errorStatus; + + public String getErrorCode() { + return errorStatus.getCode(); + } + + public String getErrorReason() { + return errorStatus.getMessage(); + } + + public HttpStatus getHttpStatus() { + return errorStatus.getHttpStatus(); + } +} diff --git a/src/main/java/fairytale/tbd/global/response/ApiResponse.java b/src/main/java/fairytale/tbd/global/response/ApiResponse.java new file mode 100644 index 0000000..5658e63 --- /dev/null +++ b/src/main/java/fairytale/tbd/global/response/ApiResponse.java @@ -0,0 +1,35 @@ +package fairytale.tbd.global.response; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import fairytale.tbd.global.enums.statuscode.BaseCode; +import fairytale.tbd.global.enums.statuscode.SuccessStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + private final Boolean isSuccess; + private final String code; + + private final String message; + private T result; + + // 성공한 경우 응답 생성 + + public static ApiResponse onSuccess(T result) { + return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), result); + } + + public static ApiResponse of(boolean isSuccess, BaseCode code, T result) { + return new ApiResponse<>(isSuccess, code.getCode(), code.getMessage(), result); + } + + // 실패한 경우 응답 생성 + public static ApiResponse onFailure(String code, String message, T data) { + return new ApiResponse<>(false, code, message, data); + } +}