diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy_beanstalk.yml similarity index 100% rename from .github/workflows/dev_deploy.yml rename to .github/workflows/dev_deploy_beanstalk.yml diff --git a/.github/workflows/dev_deploy_DEB.yml b/.github/workflows/dev_deploy_docker_beanstalk.yml similarity index 100% rename from .github/workflows/dev_deploy_DEB.yml rename to .github/workflows/dev_deploy_docker_beanstalk.yml diff --git a/build.gradle b/build.gradle index c094a9e..03057f5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.3.0' - id 'io.spring.dependency-management' version '1.1.5' + id 'org.springframework.boot' version '2.7.7' + id 'io.spring.dependency-management' version '1.1.4' } -group = 'khu.bigdata' +group = 'com.cmc' version = '0.0.1-SNAPSHOT' java { @@ -27,10 +27,10 @@ repositories { dependencies { implementation 'javax.servlet:javax.servlet-api:4.0.1' - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springdoc:springdoc-openapi-ui:1.6.11' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/src/main/java/com/cmc/suppin/global/config/SwaggerConfig.java b/src/main/java/com/cmc/suppin/global/config/SwaggerConfig.java index 70170a0..6e51466 100644 --- a/src/main/java/com/cmc/suppin/global/config/SwaggerConfig.java +++ b/src/main/java/com/cmc/suppin/global/config/SwaggerConfig.java @@ -15,8 +15,8 @@ public class SwaggerConfig { @Bean public OpenAPI suppinAPI() { Info info = new Info() - .title("suppin API") - .description("suppin API 명세서") + .title("Suppin API") + .description("Suppin API 명세서") .version("1.0.0"); String jwtSchemeName = "JWT TOKEN"; diff --git a/src/main/java/com/cmc/suppin/global/config/WebConfig.java b/src/main/java/com/cmc/suppin/global/config/WebConfig.java new file mode 100644 index 0000000..5f00f39 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/config/WebConfig.java @@ -0,0 +1,34 @@ +package com.cmc.suppin.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; + + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + + + @Bean + public UrlBasedCorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource corsConfigSource = new UrlBasedCorsConfigurationSource(); + + + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.addAllowedOriginPattern("*"); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + corsConfigSource.registerCorsConfiguration("/**", configuration); + return corsConfigSource; + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/BaseCode.java b/src/main/java/com/cmc/suppin/global/exception/BaseCode.java new file mode 100644 index 0000000..b1efb66 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/BaseCode.java @@ -0,0 +1,7 @@ +package com.cmc.suppin.global.exception; + +public interface BaseCode { + public ReasonDTO getReason(); + + public ReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/com/cmc/suppin/global/exception/BaseErrorCode.java b/src/main/java/com/cmc/suppin/global/exception/BaseErrorCode.java new file mode 100644 index 0000000..29b4a06 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/BaseErrorCode.java @@ -0,0 +1,8 @@ +package com.cmc.suppin.global.exception; + +public interface BaseErrorCode { + + public ErrorReasonDTO getReason(); + + public ErrorReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/com/cmc/suppin/global/exception/ErrorReasonDTO.java b/src/main/java/com/cmc/suppin/global/exception/ErrorReasonDTO.java new file mode 100644 index 0000000..4f911a2 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/ErrorReasonDTO.java @@ -0,0 +1,23 @@ +package com.cmc.suppin.global.exception; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ErrorReasonDTO { + + private final HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; + + private final Integer status; + private final String reason; + + public boolean getIsSuccess() { + return isSuccess; + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/ExceptionAdvice.java b/src/main/java/com/cmc/suppin/global/exception/ExceptionAdvice.java new file mode 100644 index 0000000..8a76b7b --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/ExceptionAdvice.java @@ -0,0 +1,115 @@ +package com.cmc.suppin.global.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +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 com.cmc.suppin.global.exception.status.ErrorStatus; +import com.cmc.suppin.global.presentation.ApiResponse; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolationException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}) +public class ExceptionAdvice extends ResponseEntityExceptionHandler { + + @org.springframework.web.bind.annotation.ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = e.getConstraintViolations().stream() + .map(constraintViolation -> constraintViolation.getMessage()) + .findFirst() + .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); + + return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request); + } + + @Override + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, HttpHeaders headers, HttpStatus status, WebRequest request) { + + Map errors = new LinkedHashMap<>(); + + e.getBindingResult().getFieldErrors().stream() + .forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(e, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"), request, errors); + } + + @org.springframework.web.bind.annotation.ExceptionHandler + public ResponseEntity exception(Exception e, WebRequest request) { + e.printStackTrace(); + + return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage()); + } + + @ExceptionHandler(value = GeneralException.class) + public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { + ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); + return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request); + } + + private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) { + ApiResponse body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null); +// e.printStackTrace(); + + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal( + e, + body, + headers, + reason.getHttpStatus(), + webRequest + ); + } + + private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorPoint); + return super.handleExceptionInternal( + e, + 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 + ); + } + + private ResponseEntity handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, WebRequest request) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/GeneralException.java b/src/main/java/com/cmc/suppin/global/exception/GeneralException.java new file mode 100644 index 0000000..bd2bddc --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/GeneralException.java @@ -0,0 +1,19 @@ +package com.cmc.suppin.global.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException { + + private BaseErrorCode code; + + public ErrorReasonDTO getErrorReason() { + return this.code.getReason(); + } + + public ErrorReasonDTO getErrorReasonHttpStatus() { + return this.code.getReasonHttpStatus(); + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/ReasonDTO.java b/src/main/java/com/cmc/suppin/global/exception/ReasonDTO.java new file mode 100644 index 0000000..9d4bfd0 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/ReasonDTO.java @@ -0,0 +1,20 @@ +package com.cmc.suppin.global.exception; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ReasonDTO { + + private HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; + + public boolean getIsSuccess() { + return isSuccess; + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/cmc/suppin/global/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..dd0bb1e --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,4 @@ +package com.cmc.suppin.global.exception.handler; + +public class GlobalExceptionHandler { +} diff --git a/src/main/java/com/cmc/suppin/global/exception/handler/JwtHandler.java b/src/main/java/com/cmc/suppin/global/exception/handler/JwtHandler.java new file mode 100644 index 0000000..fd32363 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/handler/JwtHandler.java @@ -0,0 +1,10 @@ +package com.cmc.suppin.global.exception.handler; + +import com.cmc.suppin.global.exception.BaseErrorCode; +import com.cmc.suppin.global.exception.GeneralException; + +public class JwtHandler extends GeneralException { + public JwtHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/status/ErrorStatus.java b/src/main/java/com/cmc/suppin/global/exception/status/ErrorStatus.java new file mode 100644 index 0000000..2188d1e --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/status/ErrorStatus.java @@ -0,0 +1,60 @@ +package com.cmc.suppin.global.exception.status; + +import com.cmc.suppin.global.exception.BaseErrorCode; +import com.cmc.suppin.global.exception.ErrorReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + + // 가장 일반적인 에러 + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), + _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + + + // test + TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "테스트"), + + // Member + MEMBER_NICKNAME_DUPLICATED(HttpStatus.BAD_REQUEST, "MEMBER4001", "중복된 닉네임 입니다."), + MEMBER_PASSWORD_ERROR(HttpStatus.BAD_REQUEST, "MEMBER4002", "비밀번호가 잘못되었습니다."), + + //JWT + JWT_BAD_REQUEST(HttpStatus.UNAUTHORIZED, "JWT4001", "잘못된 JWT 서명입니다."), + JWT_ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "JWT4002", "액세스 토큰이 만료되었습니다."), + JWT_REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "JWT4003", "리프레시 토큰이 만료되었습니다. 다시 로그인하시기 바랍니다."), + JWT_UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "JWT4004", "지원하지 않는 JWT 토큰입니다."), + JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "JWT4005", "유효한 JWT 토큰이 없습니다."), + + // 페이징 관련 에러 + PAGE_NEGATIVE_INPUT(HttpStatus.BAD_REQUEST, "PAGE4001", "페이지 번호는 1이상의 숫자여야 합니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build(); + } +} diff --git a/src/main/java/com/cmc/suppin/global/exception/status/SuccessStatus.java b/src/main/java/com/cmc/suppin/global/exception/status/SuccessStatus.java new file mode 100644 index 0000000..9b9f06b --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/exception/status/SuccessStatus.java @@ -0,0 +1,40 @@ +package com.cmc.suppin.global.exception.status; + +import com.cmc.suppin.global.exception.BaseCode; +import com.cmc.suppin.global.exception.ReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum SuccessStatus implements BaseCode { + _OK(HttpStatus.OK, "COMMON200", "성공입니다."), + + //Member + MEMBER_DELETE_SUCCESS(HttpStatus.OK, "MEMBER2001", "회원 탈퇴 성공입니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReason() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .build(); + } + + @Override + public ReasonDTO getReasonHttpStatus() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .httpStatus(httpStatus) + .build(); + } +} diff --git a/src/main/java/com/cmc/suppin/global/presentation/ApiResponse.java b/src/main/java/com/cmc/suppin/global/presentation/ApiResponse.java new file mode 100644 index 0000000..d239b00 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/presentation/ApiResponse.java @@ -0,0 +1,33 @@ +package com.cmc.suppin.global.presentation; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; +import com.cmc.suppin.global.exception.BaseCode; +import com.cmc.suppin.global.exception.status.SuccessStatus; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + @JsonPropertyOrder("isSuccess") + private final Boolean isSuccess; + private final String code; + private final String message; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T result; + + public static ApiResponse onSuccess(T result) { + return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), result); + } + + public static ApiResponse of(BaseCode code, T result) { + return new ApiResponse<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), result); + } + + public static ApiResponse onFailure(String code, String message, T data) { + return new ApiResponse<>(false, code, message, data); + } +} diff --git a/src/main/java/com/cmc/suppin/global/presentation/ErrorResponse.java b/src/main/java/com/cmc/suppin/global/presentation/ErrorResponse.java new file mode 100644 index 0000000..f50e133 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/presentation/ErrorResponse.java @@ -0,0 +1,40 @@ +package com.cmc.suppin.global.presentation; + +import com.cmc.suppin.global.exception.ErrorReasonDTO; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@JsonPropertyOrder({"isSuccess", "code", "message", "status", "timeStamp", "path"}) +public class ErrorResponse { + + @JsonProperty("isSuccess") + private final boolean success; + + private final int status; + private final String code; + private final String message; + private final LocalDateTime timeStamp; + private final String path; + + public ErrorResponse(ErrorReasonDTO errorReason, String path) { + this.success = false; + this.status = errorReason.getStatus(); + this.code = errorReason.getCode(); + this.message = errorReason.getReason(); + this.timeStamp = LocalDateTime.now(); + this.path = path; + } + + public ErrorResponse(int status, String code, String reason, String path) { + this.success = false; + this.status = status; + this.code = code; + this.message = reason; + this.timeStamp = LocalDateTime.now(); + this.path = path; + } +} diff --git a/src/main/java/com/cmc/suppin/global/presentation/HealthCheck.java b/src/main/java/com/cmc/suppin/global/presentation/HealthCheck.java new file mode 100644 index 0000000..9825f8a --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/presentation/HealthCheck.java @@ -0,0 +1,12 @@ +package com.cmc.suppin.global.presentation; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthCheck { + @GetMapping("/health") + public String healthCheck() { + return "OK, healthy!"; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 6fb83d3..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=suppin diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..7b1d301 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,37 @@ +spring: + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + show_sql: true + format_sql: true + +# multipart 파일 용량 늘려주는 부분 +servlet: + multipart: + max-file-size: 200MB + max-request-size: 300MB + +logging: + level: + root: INFO + org: + hibernate: + SQL: DEBUG + type: trace + +springdoc: + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + path: / + disable-swagger-default-url: true + display-request-duration: true + operations-sorter: alpha