diff --git a/src/main/java/clap/server/adapter/inbound/security/LoginAttemptFilter.java b/src/main/java/clap/server/adapter/inbound/security/LoginAttemptFilter.java index ab27a20d..33de02e5 100644 --- a/src/main/java/clap/server/adapter/inbound/security/LoginAttemptFilter.java +++ b/src/main/java/clap/server/adapter/inbound/security/LoginAttemptFilter.java @@ -2,7 +2,7 @@ import clap.server.application.service.auth.LoginAttemptService; import clap.server.exception.AuthException; -import clap.server.exception.code.CommonErrorCode; +import clap.server.exception.code.GlobalErrorCode; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -35,7 +35,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (request.getRequestURI().equals(LOGIN_ENDPOINT)) { if (sessionId == null) { - throw new AuthException(CommonErrorCode.BAD_REQUEST); + throw new AuthException(GlobalErrorCode.BAD_REQUEST); } loginAttemptService.checkAccountIsLocked(sessionId); } diff --git a/src/main/java/clap/server/adapter/inbound/security/filter/JwtErrorCodeUtil.java b/src/main/java/clap/server/adapter/inbound/security/filter/JwtErrorCodeUtil.java index 3a342ecb..b4fcb6c4 100644 --- a/src/main/java/clap/server/adapter/inbound/security/filter/JwtErrorCodeUtil.java +++ b/src/main/java/clap/server/adapter/inbound/security/filter/JwtErrorCodeUtil.java @@ -2,7 +2,7 @@ import clap.server.exception.JwtException; import clap.server.exception.code.AuthErrorCode; import clap.server.exception.code.BaseErrorCode; -import clap.server.exception.code.CommonErrorCode; +import clap.server.exception.code.GlobalErrorCode; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; @@ -36,7 +36,7 @@ public static BaseErrorCode determineErrorCode(Exception exception, BaseErrorCod public static JwtException determineAuthErrorException(Exception exception) { return findAuthErrorException(exception).orElseGet( () -> { - BaseErrorCode errorCode = determineErrorCode(exception, CommonErrorCode.INTERNAL_SERVER_ERROR); + BaseErrorCode errorCode = determineErrorCode(exception, GlobalErrorCode.INTERNAL_SERVER_ERROR); log.debug(exception.getMessage(), exception); return new JwtException(errorCode); } diff --git a/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java b/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java new file mode 100644 index 00000000..165b2bd2 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/example/ErrorExampleController.java @@ -0,0 +1,76 @@ +package clap.server.adapter.inbound.web.example; + +import clap.server.common.annotation.architecture.WebAdapter; +import clap.server.common.annotation.swagger.ApiErrorCodes; +import clap.server.common.annotation.swagger.DevelopOnlyApi; +import clap.server.exception.code.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Tag(name = "*. 에러 응답") +@WebAdapter +@RequestMapping("/api/examples") +public class ErrorExampleController { + + @GetMapping("/global") + @DevelopOnlyApi + @Operation(summary = "글로벌 (aop, 서버 내부 오류등) 관련 에러 코드 나열") + @ApiErrorCodes(GlobalErrorCode.class) + public void getGlobalErrorCode() {} + + @GetMapping("/member") + @DevelopOnlyApi + @Operation(summary = "회원 도메인 관련 에러 코드 나열") + @ApiErrorCodes(MemberErrorCode.class) + public void getMemberErrorCode() {} + + @GetMapping("/auth") + @DevelopOnlyApi + @Operation(summary = "인증 및 인가 관련 에러 코드 나열") + @ApiErrorCodes(MemberErrorCode.class) + public void getAuthErrorCode() {} + + @GetMapping("/task") + @DevelopOnlyApi + @Operation(summary = "작업 도메인 관련 에러 코드 나열") + @ApiErrorCodes(TaskErrorCode.class) + public void getTaskErrorCode() {} + + @GetMapping("/notification") + @DevelopOnlyApi + @Operation(summary = "알림 도메인 및 웹훅 관련 에러 코드 나열") + @ApiErrorCodes(TaskErrorCode.class) + public void getNotificationErrorCode() {} + + @GetMapping("/comment") + @DevelopOnlyApi + @Operation(summary = "댓글 도메인 관련 에러 코드 나열") + @ApiErrorCodes(CommentErrorCode.class) + public void getCommentErrorCode() {} + + @GetMapping("/statistic") + @DevelopOnlyApi + @Operation(summary = "작업 통계 관련 에러 코드 나열") + @ApiErrorCodes(LabelErrorCode.class) + public void getStatisticsErrorCode() {} + + @GetMapping("/label") + @DevelopOnlyApi + @Operation(summary = "라벨 도메인 관련 에러 코드 나열") + @ApiErrorCodes(LabelErrorCode.class) + public void getLabelErrorCode() {} + + @GetMapping("/department") + @DevelopOnlyApi + @Operation(summary = "부서 도메인 관련 에러 코드 나열") + @ApiErrorCodes(DepartmentErrorCode.class) + public void getDepartmentErrorCode() {} + + @GetMapping("/file") + @DevelopOnlyApi + @Operation(summary = "파일 처리 관련 에러 코드 나열") + @ApiErrorCodes(FileErrorcode.class) + public void getFileErrorCode() {} +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java index 6b8f33a9..c0d01f09 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java @@ -5,7 +5,7 @@ import clap.server.config.s3.KakaoS3Config; import clap.server.common.constants.FilePathConstants; import clap.server.exception.S3Exception; -import clap.server.exception.code.AttachmentErrorcode; +import clap.server.exception.code.FileErrorcode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.multipart.MultipartFile; @@ -39,7 +39,7 @@ public String uploadSingleFile(FilePathConstants filePrefix, MultipartFile file) Files.delete(filePath); return getFileUrl(objectKey); } catch (IOException e) { - throw new S3Exception(AttachmentErrorcode.FILE_UPLOAD_REQUEST_FAILED); + throw new S3Exception(FileErrorcode.FILE_UPLOAD_REQUEST_FAILED); } } diff --git a/src/main/java/clap/server/application/service/auth/LoginAttemptService.java b/src/main/java/clap/server/application/service/auth/LoginAttemptService.java index 61c628e6..fc8be813 100644 --- a/src/main/java/clap/server/application/service/auth/LoginAttemptService.java +++ b/src/main/java/clap/server/application/service/auth/LoginAttemptService.java @@ -51,6 +51,7 @@ public void checkAccountIsLocked(String sessionId) { if (minutesSinceLastAttemptInMillis <= LOCK_TIME_DURATION) { throw new AuthException(AuthErrorCode.ACCOUNT_IS_LOCKED); } + commandLoginLogPort.deleteById(sessionId); } } diff --git a/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java b/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java index 7d79fadd..04bb3960 100644 --- a/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java +++ b/src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java @@ -9,8 +9,7 @@ import clap.server.common.utils.FileUtils; import clap.server.domain.model.member.Member; import clap.server.exception.ApplicationException; -import clap.server.exception.code.AttachmentErrorcode; -import clap.server.exception.code.MemberErrorCode; +import clap.server.exception.code.FileErrorcode; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -27,7 +26,7 @@ class UpdateMemberInfoService implements UpdateMemberInfoUsecase { @Override public void updateMemberInfo(Long memberId, UpdateMemberInfoRequest request, MultipartFile profileImage) throws IOException { if (!FileUtils.validImageFile(profileImage.getInputStream())) { - throw new ApplicationException(AttachmentErrorcode.UNSUPPORTED_FILE_TYPE); + throw new ApplicationException(FileErrorcode.UNSUPPORTED_FILE_TYPE); } Member member = memberService.findActiveMember(memberId); String profileImageUrl = s3UploadPort.uploadSingleFile(FilePathConstants.MEMBER_IMAGE, profileImage); diff --git a/src/main/java/clap/server/common/annotation/swagger/ApiErrorCodes.java b/src/main/java/clap/server/common/annotation/swagger/ApiErrorCodes.java new file mode 100644 index 00000000..cc6c5389 --- /dev/null +++ b/src/main/java/clap/server/common/annotation/swagger/ApiErrorCodes.java @@ -0,0 +1,14 @@ +package clap.server.common.annotation.swagger; + +import clap.server.exception.code.BaseErrorCode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiErrorCodes { + Class value(); +} \ No newline at end of file diff --git a/src/main/java/clap/server/common/annotation/swagger/DevelopOnlyApi.java b/src/main/java/clap/server/common/annotation/swagger/DevelopOnlyApi.java new file mode 100644 index 00000000..191f7bb6 --- /dev/null +++ b/src/main/java/clap/server/common/annotation/swagger/DevelopOnlyApi.java @@ -0,0 +1,10 @@ +package clap.server.common.annotation.swagger; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DevelopOnlyApi {} \ No newline at end of file diff --git a/src/main/java/clap/server/config/swagger/ErrorExample.java b/src/main/java/clap/server/config/swagger/ErrorExample.java new file mode 100644 index 00000000..ef914776 --- /dev/null +++ b/src/main/java/clap/server/config/swagger/ErrorExample.java @@ -0,0 +1,8 @@ +package clap.server.config.swagger; + +public record ErrorExample( + int code, + String customCode, + String message +) { +} diff --git a/src/main/java/clap/server/config/swagger/SwaggerConfig.java b/src/main/java/clap/server/config/swagger/SwaggerConfig.java index 9e3f1146..ece403dd 100644 --- a/src/main/java/clap/server/config/swagger/SwaggerConfig.java +++ b/src/main/java/clap/server/config/swagger/SwaggerConfig.java @@ -1,19 +1,32 @@ package clap.server.config.swagger; +import clap.server.common.annotation.swagger.ApiErrorCodes; +import clap.server.exception.code.BaseErrorCode; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; +import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.HandlerMethod; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import static clap.server.common.constants.AuthConstants.AUTHORIZATION; +import static java.util.stream.Collectors.groupingBy; @Configuration public class SwaggerConfig { @@ -59,4 +72,77 @@ private static Components getComponents() { return new Components() .addSecuritySchemes(AUTHORIZATION.getValue(), securityScheme); } + + @Bean + public OperationCustomizer customize() { + return (Operation operation, HandlerMethod handlerMethod) -> { + ApiErrorCodes apiErrorCodeExample = + handlerMethod.getMethodAnnotation(ApiErrorCodes.class); + + if (apiErrorCodeExample != null) { + generateErrorCodeResponse(operation, apiErrorCodeExample.value()); + } + return operation; + }; + } + + private void generateErrorCodeResponse(Operation operation, Class type) { + ApiResponses responses = operation.getResponses(); + BaseErrorCode[] errorCodes = type.getEnumConstants(); + Map> statusWithExampleHolders = Arrays.stream(errorCodes) + .map(errorCode -> ErrorExampleHolder.builder() + .example(getSwaggerExample(errorCode)) + .name(errorCode.name()) + .code(errorCode.getHttpStatus().value()) + .build()) + .collect(groupingBy(ErrorExampleHolder::getCode)); + + addExamplesToResponses(responses, statusWithExampleHolders); + } + + + /** + * {@code @ApiErrorCodes} 어노테이션이 존재할 경우 {@code ApiResponses}에 {@code Example}를 추가하는 메소드 + * + * @param responses + * @param statusWithExampleHolders + */ + private void addExamplesToResponses( + ApiResponses responses, + Map> statusWithExampleHolders + ) { + statusWithExampleHolders.forEach( + (status, v) -> { + Content content = new Content(); + MediaType mediaType = new MediaType(); + ApiResponse apiResponse = new ApiResponse(); + + v.forEach( + exampleHolder -> mediaType.addExamples( + exampleHolder.getName(), + exampleHolder.getExample() + ) + ); + + content.addMediaType("application/json", mediaType); + apiResponse.setContent(content); + responses.addApiResponse(String.valueOf(status), apiResponse); + }); + } + + + /** + * {@code BaseErrorCode}를 통해 {@code Example}를 생성하는 메소드 + * + * @param errorCode + * @return + */ + private Example getSwaggerExample(BaseErrorCode errorCode) { + ErrorExample errorExample = new ErrorExample(errorCode.getHttpStatus().value(), errorCode.getCustomCode(), errorCode.getMessage()); + Example example = new Example(); + example.setValue(errorExample); + + return example; + } + } \ No newline at end of file diff --git a/src/main/java/clap/server/exception/ExceptionAdvice.java b/src/main/java/clap/server/exception/ExceptionAdvice.java index 1a9c0e78..ba8dc8f5 100644 --- a/src/main/java/clap/server/exception/ExceptionAdvice.java +++ b/src/main/java/clap/server/exception/ExceptionAdvice.java @@ -2,7 +2,7 @@ import clap.server.exception.code.AuthErrorCode; import clap.server.exception.code.BaseErrorCode; -import clap.server.exception.code.CommonErrorCode; +import clap.server.exception.code.GlobalErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -48,7 +48,7 @@ public ResponseEntity handleMethodArgumentNotValid( return handleExceptionInternalArgs( e, HttpHeaders.EMPTY, - CommonErrorCode.BAD_REQUEST, + GlobalErrorCode.BAD_REQUEST, request, errors ); @@ -61,7 +61,7 @@ public ResponseEntity validation(ConstraintViolationException e, WebRequ .findFirst() .orElseThrow(() -> new RuntimeException("ConstraintViolationException Error")); - return handleExceptionInternalConstraint(e, CommonErrorCode.valueOf(errorMessage), HttpHeaders.EMPTY, request); + return handleExceptionInternalConstraint(e, GlobalErrorCode.valueOf(errorMessage), HttpHeaders.EMPTY, request); } @ExceptionHandler @@ -70,9 +70,9 @@ public ResponseEntity exception(Exception e, WebRequest request) { return handleExceptionInternalFalse( e, - CommonErrorCode.INTERNAL_SERVER_ERROR, + GlobalErrorCode.INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, - CommonErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus(), + GlobalErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage() ); diff --git a/src/main/java/clap/server/exception/code/AttachmentErrorcode.java b/src/main/java/clap/server/exception/code/FileErrorcode.java similarity index 89% rename from src/main/java/clap/server/exception/code/AttachmentErrorcode.java rename to src/main/java/clap/server/exception/code/FileErrorcode.java index 8992f41c..1e3e47fc 100644 --- a/src/main/java/clap/server/exception/code/AttachmentErrorcode.java +++ b/src/main/java/clap/server/exception/code/FileErrorcode.java @@ -6,7 +6,7 @@ @Getter @RequiredArgsConstructor -public enum AttachmentErrorcode implements BaseErrorCode { +public enum FileErrorcode implements BaseErrorCode { FILE_UPLOAD_REQUEST_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_001", "파일 업로드에 실패하였습니다."), UNSUPPORTED_FILE_TYPE(HttpStatus.BAD_REQUEST, "FILE_002", "이미지가 아닌 파일 유형입니다."),; diff --git a/src/main/java/clap/server/exception/code/CommonErrorCode.java b/src/main/java/clap/server/exception/code/GlobalErrorCode.java similarity index 93% rename from src/main/java/clap/server/exception/code/CommonErrorCode.java rename to src/main/java/clap/server/exception/code/GlobalErrorCode.java index f2c7a67c..6980ff9c 100644 --- a/src/main/java/clap/server/exception/code/CommonErrorCode.java +++ b/src/main/java/clap/server/exception/code/GlobalErrorCode.java @@ -6,7 +6,7 @@ @Getter @AllArgsConstructor -public enum CommonErrorCode implements BaseErrorCode { +public enum GlobalErrorCode implements BaseErrorCode { /** * Common Error diff --git a/src/main/java/clap/server/exception/code/StatisticsErrorCode.java b/src/main/java/clap/server/exception/code/StatisticsErrorCode.java index 8a808121..b056e963 100644 --- a/src/main/java/clap/server/exception/code/StatisticsErrorCode.java +++ b/src/main/java/clap/server/exception/code/StatisticsErrorCode.java @@ -7,7 +7,7 @@ @Getter @RequiredArgsConstructor public enum StatisticsErrorCode implements BaseErrorCode{ - STATISTICS_BAD_REQUEST(HttpStatus.BAD_REQUEST, "STATISTICS_001", "잘못된 통계 조회 파라미터 입력."); + STATISTICS_BAD_REQUEST(HttpStatus.BAD_REQUEST, "STATISTICS_001", "잘못된 통계 조회 파라미터 입력입니다."); private final HttpStatus httpStatus; private final String customCode;