Skip to content

Commit 1712ff8

Browse files
authored
Merge pull request #200 from TaskFlow-CLAP/CLAP-193
CLAP-193 Swagger 에러 응답 명세 컨트롤러 추가
2 parents 483c166 + 10fba30 commit 1712ff8

File tree

14 files changed

+211
-17
lines changed

14 files changed

+211
-17
lines changed

src/main/java/clap/server/adapter/inbound/security/LoginAttemptFilter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import clap.server.application.service.auth.LoginAttemptService;
44
import clap.server.exception.AuthException;
5-
import clap.server.exception.code.CommonErrorCode;
5+
import clap.server.exception.code.GlobalErrorCode;
66
import jakarta.servlet.FilterChain;
77
import jakarta.servlet.ServletException;
88
import jakarta.servlet.http.HttpServletRequest;
@@ -35,7 +35,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
3535

3636
if (request.getRequestURI().equals(LOGIN_ENDPOINT)) {
3737
if (sessionId == null) {
38-
throw new AuthException(CommonErrorCode.BAD_REQUEST);
38+
throw new AuthException(GlobalErrorCode.BAD_REQUEST);
3939
}
4040
loginAttemptService.checkAccountIsLocked(sessionId);
4141
}

src/main/java/clap/server/adapter/inbound/security/filter/JwtErrorCodeUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import clap.server.exception.JwtException;
33
import clap.server.exception.code.AuthErrorCode;
44
import clap.server.exception.code.BaseErrorCode;
5-
import clap.server.exception.code.CommonErrorCode;
5+
import clap.server.exception.code.GlobalErrorCode;
66
import io.jsonwebtoken.ExpiredJwtException;
77
import io.jsonwebtoken.MalformedJwtException;
88
import io.jsonwebtoken.UnsupportedJwtException;
@@ -36,7 +36,7 @@ public static BaseErrorCode determineErrorCode(Exception exception, BaseErrorCod
3636
public static JwtException determineAuthErrorException(Exception exception) {
3737
return findAuthErrorException(exception).orElseGet(
3838
() -> {
39-
BaseErrorCode errorCode = determineErrorCode(exception, CommonErrorCode.INTERNAL_SERVER_ERROR);
39+
BaseErrorCode errorCode = determineErrorCode(exception, GlobalErrorCode.INTERNAL_SERVER_ERROR);
4040
log.debug(exception.getMessage(), exception);
4141
return new JwtException(errorCode);
4242
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package clap.server.adapter.inbound.web.example;
2+
3+
import clap.server.common.annotation.architecture.WebAdapter;
4+
import clap.server.common.annotation.swagger.ApiErrorCodes;
5+
import clap.server.common.annotation.swagger.DevelopOnlyApi;
6+
import clap.server.exception.code.*;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
12+
@Tag(name = "*. 에러 응답")
13+
@WebAdapter
14+
@RequestMapping("/api/examples")
15+
public class ErrorExampleController {
16+
17+
@GetMapping("/global")
18+
@DevelopOnlyApi
19+
@Operation(summary = "글로벌 (aop, 서버 내부 오류등) 관련 에러 코드 나열")
20+
@ApiErrorCodes(GlobalErrorCode.class)
21+
public void getGlobalErrorCode() {}
22+
23+
@GetMapping("/member")
24+
@DevelopOnlyApi
25+
@Operation(summary = "회원 도메인 관련 에러 코드 나열")
26+
@ApiErrorCodes(MemberErrorCode.class)
27+
public void getMemberErrorCode() {}
28+
29+
@GetMapping("/auth")
30+
@DevelopOnlyApi
31+
@Operation(summary = "인증 및 인가 관련 에러 코드 나열")
32+
@ApiErrorCodes(MemberErrorCode.class)
33+
public void getAuthErrorCode() {}
34+
35+
@GetMapping("/task")
36+
@DevelopOnlyApi
37+
@Operation(summary = "작업 도메인 관련 에러 코드 나열")
38+
@ApiErrorCodes(TaskErrorCode.class)
39+
public void getTaskErrorCode() {}
40+
41+
@GetMapping("/notification")
42+
@DevelopOnlyApi
43+
@Operation(summary = "알림 도메인 및 웹훅 관련 에러 코드 나열")
44+
@ApiErrorCodes(TaskErrorCode.class)
45+
public void getNotificationErrorCode() {}
46+
47+
@GetMapping("/comment")
48+
@DevelopOnlyApi
49+
@Operation(summary = "댓글 도메인 관련 에러 코드 나열")
50+
@ApiErrorCodes(CommentErrorCode.class)
51+
public void getCommentErrorCode() {}
52+
53+
@GetMapping("/statistic")
54+
@DevelopOnlyApi
55+
@Operation(summary = "작업 통계 관련 에러 코드 나열")
56+
@ApiErrorCodes(LabelErrorCode.class)
57+
public void getStatisticsErrorCode() {}
58+
59+
@GetMapping("/label")
60+
@DevelopOnlyApi
61+
@Operation(summary = "라벨 도메인 관련 에러 코드 나열")
62+
@ApiErrorCodes(LabelErrorCode.class)
63+
public void getLabelErrorCode() {}
64+
65+
@GetMapping("/department")
66+
@DevelopOnlyApi
67+
@Operation(summary = "부서 도메인 관련 에러 코드 나열")
68+
@ApiErrorCodes(DepartmentErrorCode.class)
69+
public void getDepartmentErrorCode() {}
70+
71+
@GetMapping("/file")
72+
@DevelopOnlyApi
73+
@Operation(summary = "파일 처리 관련 에러 코드 나열")
74+
@ApiErrorCodes(FileErrorcode.class)
75+
public void getFileErrorCode() {}
76+
}

src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import clap.server.config.s3.KakaoS3Config;
66
import clap.server.common.constants.FilePathConstants;
77
import clap.server.exception.S3Exception;
8-
import clap.server.exception.code.AttachmentErrorcode;
8+
import clap.server.exception.code.FileErrorcode;
99
import lombok.RequiredArgsConstructor;
1010
import lombok.extern.slf4j.Slf4j;
1111
import org.springframework.web.multipart.MultipartFile;
@@ -39,7 +39,7 @@ public String uploadSingleFile(FilePathConstants filePrefix, MultipartFile file)
3939
Files.delete(filePath);
4040
return getFileUrl(objectKey);
4141
} catch (IOException e) {
42-
throw new S3Exception(AttachmentErrorcode.FILE_UPLOAD_REQUEST_FAILED);
42+
throw new S3Exception(FileErrorcode.FILE_UPLOAD_REQUEST_FAILED);
4343
}
4444
}
4545

src/main/java/clap/server/application/service/auth/LoginAttemptService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public void checkAccountIsLocked(String sessionId) {
5151
if (minutesSinceLastAttemptInMillis <= LOCK_TIME_DURATION) {
5252
throw new AuthException(AuthErrorCode.ACCOUNT_IS_LOCKED);
5353
}
54+
commandLoginLogPort.deleteById(sessionId);
5455
}
5556
}
5657

src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
import clap.server.common.utils.FileUtils;
1010
import clap.server.domain.model.member.Member;
1111
import clap.server.exception.ApplicationException;
12-
import clap.server.exception.code.AttachmentErrorcode;
13-
import clap.server.exception.code.MemberErrorCode;
12+
import clap.server.exception.code.FileErrorcode;
1413
import lombok.RequiredArgsConstructor;
1514
import org.springframework.transaction.annotation.Transactional;
1615
import org.springframework.web.multipart.MultipartFile;
@@ -27,7 +26,7 @@ class UpdateMemberInfoService implements UpdateMemberInfoUsecase {
2726
@Override
2827
public void updateMemberInfo(Long memberId, UpdateMemberInfoRequest request, MultipartFile profileImage) throws IOException {
2928
if (!FileUtils.validImageFile(profileImage.getInputStream())) {
30-
throw new ApplicationException(AttachmentErrorcode.UNSUPPORTED_FILE_TYPE);
29+
throw new ApplicationException(FileErrorcode.UNSUPPORTED_FILE_TYPE);
3130
}
3231
Member member = memberService.findActiveMember(memberId);
3332
String profileImageUrl = s3UploadPort.uploadSingleFile(FilePathConstants.MEMBER_IMAGE, profileImage);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package clap.server.common.annotation.swagger;
2+
3+
import clap.server.exception.code.BaseErrorCode;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
@Target(ElementType.METHOD)
11+
@Retention(RetentionPolicy.RUNTIME)
12+
public @interface ApiErrorCodes {
13+
Class<? extends BaseErrorCode> value();
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package clap.server.common.annotation.swagger;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target({ElementType.METHOD})
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface DevelopOnlyApi {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package clap.server.config.swagger;
2+
3+
public record ErrorExample(
4+
int code,
5+
String customCode,
6+
String message
7+
) {
8+
}

src/main/java/clap/server/config/swagger/SwaggerConfig.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
package clap.server.config.swagger;
22

3+
import clap.server.common.annotation.swagger.ApiErrorCodes;
4+
import clap.server.exception.code.BaseErrorCode;
35
import io.swagger.v3.oas.models.Components;
46
import io.swagger.v3.oas.models.OpenAPI;
7+
import io.swagger.v3.oas.models.Operation;
8+
import io.swagger.v3.oas.models.examples.Example;
59
import io.swagger.v3.oas.models.info.Info;
10+
import io.swagger.v3.oas.models.media.Content;
11+
import io.swagger.v3.oas.models.media.MediaType;
12+
import io.swagger.v3.oas.models.responses.ApiResponse;
13+
import io.swagger.v3.oas.models.responses.ApiResponses;
614
import io.swagger.v3.oas.models.security.SecurityRequirement;
715
import io.swagger.v3.oas.models.security.SecurityScheme;
816
import io.swagger.v3.oas.models.servers.Server;
17+
import org.springdoc.core.customizers.OperationCustomizer;
918
import org.springframework.beans.factory.annotation.Value;
1019
import org.springframework.context.annotation.Bean;
1120
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.web.method.HandlerMethod;
1222

23+
import java.util.Arrays;
1324
import java.util.Collections;
1425
import java.util.List;
26+
import java.util.Map;
1527

1628
import static clap.server.common.constants.AuthConstants.AUTHORIZATION;
29+
import static java.util.stream.Collectors.groupingBy;
1730

1831
@Configuration
1932
public class SwaggerConfig {
@@ -59,4 +72,77 @@ private static Components getComponents() {
5972
return new Components()
6073
.addSecuritySchemes(AUTHORIZATION.getValue(), securityScheme);
6174
}
75+
76+
@Bean
77+
public OperationCustomizer customize() {
78+
return (Operation operation, HandlerMethod handlerMethod) -> {
79+
ApiErrorCodes apiErrorCodeExample =
80+
handlerMethod.getMethodAnnotation(ApiErrorCodes.class);
81+
82+
if (apiErrorCodeExample != null) {
83+
generateErrorCodeResponse(operation, apiErrorCodeExample.value());
84+
}
85+
return operation;
86+
};
87+
}
88+
89+
private void generateErrorCodeResponse(Operation operation, Class<? extends BaseErrorCode> type) {
90+
ApiResponses responses = operation.getResponses();
91+
BaseErrorCode[] errorCodes = type.getEnumConstants();
92+
Map<Integer, List<ErrorExampleHolder>> statusWithExampleHolders = Arrays.stream(errorCodes)
93+
.map(errorCode -> ErrorExampleHolder.builder()
94+
.example(getSwaggerExample(errorCode))
95+
.name(errorCode.name())
96+
.code(errorCode.getHttpStatus().value())
97+
.build())
98+
.collect(groupingBy(ErrorExampleHolder::getCode));
99+
100+
addExamplesToResponses(responses, statusWithExampleHolders);
101+
}
102+
103+
104+
/**
105+
* {@code @ApiErrorCodes} 어노테이션이 존재할 경우 {@code ApiResponses}에 {@code Example}를 추가하는 메소드
106+
*
107+
* @param responses
108+
* @param statusWithExampleHolders
109+
*/
110+
private void addExamplesToResponses(
111+
ApiResponses responses,
112+
Map<Integer, List<ErrorExampleHolder>> statusWithExampleHolders
113+
) {
114+
statusWithExampleHolders.forEach(
115+
(status, v) -> {
116+
Content content = new Content();
117+
MediaType mediaType = new MediaType();
118+
ApiResponse apiResponse = new ApiResponse();
119+
120+
v.forEach(
121+
exampleHolder -> mediaType.addExamples(
122+
exampleHolder.getName(),
123+
exampleHolder.getExample()
124+
)
125+
);
126+
127+
content.addMediaType("application/json", mediaType);
128+
apiResponse.setContent(content);
129+
responses.addApiResponse(String.valueOf(status), apiResponse);
130+
});
131+
}
132+
133+
134+
/**
135+
* {@code BaseErrorCode}를 통해 {@code Example}를 생성하는 메소드
136+
*
137+
* @param errorCode
138+
* @return
139+
*/
140+
private Example getSwaggerExample(BaseErrorCode errorCode) {
141+
ErrorExample errorExample = new ErrorExample(errorCode.getHttpStatus().value(), errorCode.getCustomCode(), errorCode.getMessage());
142+
Example example = new Example();
143+
example.setValue(errorExample);
144+
145+
return example;
146+
}
147+
62148
}

0 commit comments

Comments
 (0)