diff --git a/config/naver-checkstyle-rules.xml b/config/naver-checkstyle-rules.xml
index 06d3541..45acf10 100644
--- a/config/naver-checkstyle-rules.xml
+++ b/config/naver-checkstyle-rules.xml
@@ -32,9 +32,9 @@ The following rules in the Naver coding convention cannot be checked by this con
-
-
-
+
+
+
@@ -55,13 +55,13 @@ The following rules in the Naver coding convention cannot be checked by this con
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -85,25 +85,25 @@ The following rules in the Naver coding convention cannot be checked by this con
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/main/java/capstone/relation/api/auth/controller/AuthController.java b/src/main/java/capstone/relation/api/auth/controller/AuthController.java
index ce026af..1b75d53 100644
--- a/src/main/java/capstone/relation/api/auth/controller/AuthController.java
+++ b/src/main/java/capstone/relation/api/auth/controller/AuthController.java
@@ -12,9 +12,12 @@
import org.springframework.web.bind.annotation.RestController;
import capstone.relation.api.auth.AuthProvider;
+import capstone.relation.api.auth.docs.LoginAuthExceptionDocs;
+import capstone.relation.api.auth.docs.RefreshAuthExceptionDocs;
import capstone.relation.api.auth.jwt.response.RefreshTokenResponse;
import capstone.relation.api.auth.jwt.response.TokenResponse;
import capstone.relation.api.auth.service.AuthService;
+import capstone.relation.global.annotation.ApiErrorExceptionsExample;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -47,21 +50,13 @@ public void getKakaoCode(@RequestParam String code, HttpServletResponse response
@PostMapping(value = "/kakao", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "카카오 로그인 인증", description = "초대받은 경우만 있는 토큰으로 카카오를 통한 로그인 인증을 처리합니다. "
+ "초대 토큰이 없는 경우에는 `inviteToken` 없이 요청합니다.")
- @ApiResponse(responseCode = "200", description = "Successful operation",
- content = @Content(schema = @Schema(implementation = TokenResponse.class)))
+ @ApiErrorExceptionsExample(LoginAuthExceptionDocs.class)
public ResponseEntity loginWithKakaoCode(
@Parameter(description = "카카오에서 받아온 AuthorizationCode", required = true, example = "네이버에서 받아온 코드")
@RequestParam String code,
@Parameter(description = "초대된 경우에만 있는 코드", required = false, example = "초대 코드")
@RequestParam(required = false) String inviteCode) {
- System.out.println(code);
- System.out.println("카카오 로그인");
- try {
- return ResponseEntity.ok(authService.loginWithCode(AuthProvider.KAKAO, code, inviteCode));
- } catch (Exception e) {
- System.out.println(e.getMessage());
- return ResponseEntity.badRequest().build();
- }
+ return ResponseEntity.ok(authService.loginWithCode(AuthProvider.KAKAO, code, inviteCode));
}
@PostMapping(value = "/naver", produces = MediaType.APPLICATION_JSON_VALUE)
@@ -69,20 +64,15 @@ public ResponseEntity loginWithKakaoCode(
+ "초대받은 경우만 있는 토큰으로 네이버를 통한 로그인 인증을 처리합니다. 초대 토큰이 없는 경우에는 `inviteToken` 없이 요청합니다.")
@ApiResponse(responseCode = "200", description = "Successful operation",
content = @Content(schema = @Schema(implementation = TokenResponse.class)))
+ @ApiErrorExceptionsExample(LoginAuthExceptionDocs.class)
public ResponseEntity loginWithNaverCode(@RequestParam String code,
@RequestParam(required = false) String inviteCode) {
- try {
- return ResponseEntity.ok(authService.loginWithCode(AuthProvider.NAVER, code, inviteCode));
- } catch (Exception e) {
- System.out.println(e.getMessage());
- return ResponseEntity.badRequest().build();
- }
+ return ResponseEntity.ok(authService.loginWithCode(AuthProvider.NAVER, code, inviteCode));
}
- @PostMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE)
+ @PostMapping(value = "/refresh")
@Operation(summary = "AccessToken 갱신", description = "Refresh Token을 통해 AccessToken을 갱신합니다.")
- @ApiResponse(responseCode = "200", description = "Successful operation",
- content = @Content(schema = @Schema(implementation = RefreshTokenResponse.class)))
+ @ApiErrorExceptionsExample(RefreshAuthExceptionDocs.class)
public ResponseEntity refresh(@RequestHeader("Refresh") String refreshToken) {
return ResponseEntity.ok(authService.generateAccessToken(refreshToken));
}
diff --git a/src/main/java/capstone/relation/api/auth/docs/BasicAuthExceptionDocs.java b/src/main/java/capstone/relation/api/auth/docs/BasicAuthExceptionDocs.java
new file mode 100644
index 0000000..5a9cae9
--- /dev/null
+++ b/src/main/java/capstone/relation/api/auth/docs/BasicAuthExceptionDocs.java
@@ -0,0 +1,18 @@
+package capstone.relation.api.auth.docs;
+
+import capstone.relation.api.auth.exception.AuthErrorCode;
+import capstone.relation.api.auth.exception.AuthException;
+import capstone.relation.global.annotation.ExceptionDoc;
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.exception.GlobalCodeException;
+import capstone.relation.global.interfaces.SwaggerExampleExceptions;
+
+@ExceptionDoc
+public class BasicAuthExceptionDocs implements SwaggerExampleExceptions {
+ @ExplainError("엑세스 토큰이 만료된 경우 발생합니다.")
+ public GlobalCodeException 토큰_만료 = new AuthException(AuthErrorCode.TOKEN_EXPIRED);
+ @ExplainError("엑세스 토큰이 유효하지 않은 경우 발생합니다.")
+ public GlobalCodeException 토큰_유효하지_않음 = new AuthException(AuthErrorCode.INVALID_TOKEN);
+ @ExplainError("엑세스 토큰이 없는 경우 발생합니다.")
+ public GlobalCodeException 토큰_없음 = new AuthException(AuthErrorCode.ACCESS_TOKEN_NOT_EXIST);
+}
diff --git a/src/main/java/capstone/relation/api/auth/docs/LoginAuthExceptionDocs.java b/src/main/java/capstone/relation/api/auth/docs/LoginAuthExceptionDocs.java
new file mode 100644
index 0000000..171636e
--- /dev/null
+++ b/src/main/java/capstone/relation/api/auth/docs/LoginAuthExceptionDocs.java
@@ -0,0 +1,17 @@
+package capstone.relation.api.auth.docs;
+
+import capstone.relation.global.annotation.ExceptionDoc;
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.exception.GlobalCodeException;
+import capstone.relation.global.exception.GlobalErrorCode;
+import capstone.relation.global.exception.GlobalException;
+import capstone.relation.global.interfaces.SwaggerExampleExceptions;
+
+@ExceptionDoc
+public class LoginAuthExceptionDocs implements SwaggerExampleExceptions {
+ @ExplainError("로그인 서버에 보내는 요청에 실패한 경우 발생합니다.")
+ public GlobalCodeException 서버_요청_오류 = new GlobalException(GlobalErrorCode.OTHER_SERVER_BAD_REQUEST);
+
+ @ExplainError("카카오 서버에 보낸 토큰이 만료된 경우 발생합니다.")
+ public GlobalCodeException 서버_토큰_만료 = new GlobalException(GlobalErrorCode.OTHER_SERVER_EXPIRED_TOKEN);
+}
diff --git a/src/main/java/capstone/relation/api/auth/docs/RefreshAuthExceptionDocs.java b/src/main/java/capstone/relation/api/auth/docs/RefreshAuthExceptionDocs.java
new file mode 100644
index 0000000..a0515ac
--- /dev/null
+++ b/src/main/java/capstone/relation/api/auth/docs/RefreshAuthExceptionDocs.java
@@ -0,0 +1,16 @@
+package capstone.relation.api.auth.docs;
+
+import capstone.relation.api.auth.exception.AuthErrorCode;
+import capstone.relation.api.auth.exception.AuthException;
+import capstone.relation.global.annotation.ExceptionDoc;
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.exception.GlobalCodeException;
+import capstone.relation.global.interfaces.SwaggerExampleExceptions;
+
+@ExceptionDoc
+public class RefreshAuthExceptionDocs implements SwaggerExampleExceptions {
+ @ExplainError("리프레시 토큰이 만료된 경우 발생합니다.")
+ public GlobalCodeException 리프레시_토큰_만료 = new AuthException(AuthErrorCode.REFRESH_TOKEN_EXPIRED);
+ @ExplainError("리프레시 토큰이 유효하지 않은 경우 발생합니다.")
+ public GlobalCodeException 리프레시_토큰_유효하지_않음 = new AuthException(AuthErrorCode.INVALID_TOKEN);
+}
diff --git a/src/main/java/capstone/relation/api/auth/exception/AuthErrorCode.java b/src/main/java/capstone/relation/api/auth/exception/AuthErrorCode.java
index 4a7deee..a3bcc50 100644
--- a/src/main/java/capstone/relation/api/auth/exception/AuthErrorCode.java
+++ b/src/main/java/capstone/relation/api/auth/exception/AuthErrorCode.java
@@ -1,21 +1,43 @@
package capstone.relation.api.auth.exception;
-import org.springframework.http.HttpStatus;
+import static capstone.relation.global.consts.JoEunStatic.*;
+import java.lang.reflect.Field;
+import java.util.Objects;
+
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.dto.ErrorReason;
+import capstone.relation.global.exception.BaseErrorCode;
+import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
-public enum AuthErrorCode {
+@AllArgsConstructor
+public enum AuthErrorCode implements BaseErrorCode {
+
+ @ExplainError("accessToken 만료시 발생하는 오류입니다.")
+ TOKEN_EXPIRED(UNAUTHORIZED, "AUTH_401_1", "인증 시간이 만료되었습니다. 인증토큰을 재 발급 해주세요"),
+ @ExplainError("인증 토큰이 잘못됐을 때 발생하는 오류입니다.")
+ INVALID_TOKEN(UNAUTHORIZED, "AUTH_401_2", "잘못된 토큰입니다. 재 로그인 해주세요"),
- INVALID_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "access token이 이미 만료되었거나 올바르지 않습니다."),
- INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "refresh token이 이미 만료되었거나 올바르지 않습니다."),
- INVALID_WORKSPACE_STATE_USER(HttpStatus.BAD_REQUEST, "해당 워크스페이스에 가입되어 있지 않은 사용자입니다.");
+ @ExplainError("refreshToken 만료시 발생하는 오류입니다.")
+ REFRESH_TOKEN_EXPIRED(FORBIDDEN, "AUTH_403_1", "인증 시간이 만료되었습니다. 재 로그인 해주세요."),
+ @ExplainError("헤더에 올바른 accessToken을 담지않았을 때 발생하는 오류(형식 불일치 등)")
+ ACCESS_TOKEN_NOT_EXIST(FORBIDDEN, "AUTH_403_2", "알맞은 accessToken을 넣어주세요.");
- private final HttpStatus httpStatus;
- private final String message;
+ private final Integer status;
+ private final String code;
+ private final String reason;
+
+ @Override
+ public ErrorReason getErrorReason() {
+ return ErrorReason.builder().reason(reason).code(code).status(status).build();
+ }
- AuthErrorCode(HttpStatus httpStatus, String message) {
- this.httpStatus = httpStatus;
- this.message = message;
+ @Override
+ public String getExplainError() throws NoSuchFieldException {
+ Field field = this.getClass().getField(this.name());
+ ExplainError annotation = field.getAnnotation(ExplainError.class);
+ return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
}
}
diff --git a/src/main/java/capstone/relation/api/auth/exception/AuthException.java b/src/main/java/capstone/relation/api/auth/exception/AuthException.java
index 9b106d6..befaed8 100644
--- a/src/main/java/capstone/relation/api/auth/exception/AuthException.java
+++ b/src/main/java/capstone/relation/api/auth/exception/AuthException.java
@@ -1,20 +1,13 @@
package capstone.relation.api.auth.exception;
+import capstone.relation.global.exception.GlobalCodeException;
import lombok.Getter;
@Getter
-public class AuthException extends RuntimeException {
-
- private final AuthErrorCode errorCode;
+public class AuthException extends GlobalCodeException {
public AuthException(AuthErrorCode errorCode) {
- super(errorCode.getMessage());
- this.errorCode = errorCode;
- }
-
- public AuthException(AuthErrorCode errorCode, String message) {
- super(errorCode.getMessage() + " : " + message);
- this.errorCode = errorCode;
+ super(errorCode);
}
}
diff --git a/src/main/java/capstone/relation/api/auth/exception/KakaoKauthErrorCode.java b/src/main/java/capstone/relation/api/auth/exception/KakaoKauthErrorCode.java
new file mode 100644
index 0000000..49b0496
--- /dev/null
+++ b/src/main/java/capstone/relation/api/auth/exception/KakaoKauthErrorCode.java
@@ -0,0 +1,63 @@
+package capstone.relation.api.auth.exception;
+
+import static capstone.relation.global.consts.JoEunStatic.*;
+
+import java.lang.reflect.Field;
+import java.util.Objects;
+
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.dto.ErrorReason;
+import capstone.relation.global.exception.BaseErrorCode;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum KakaoKauthErrorCode implements BaseErrorCode {
+ KOE101(BAD_REQUEST, "KAKAO_KOE101", "invalid_client", "잘못된 앱 키 타입을 사용하거나 앱 키에 오타가 있을 경우"),
+ KOE009(BAD_REQUEST, "KAKAO_KOE009", "misconfigured", "등록되지 않은 플랫폼에서 액세스 토큰을 요청 하는 경우"),
+ KOE010(
+ BAD_REQUEST,
+ "KAKAO_KOE101",
+ "invalid_client",
+ "클라이언트 시크릿(Client secret) 기능을 사용하는 앱에서 토큰 요청 시 client_secret 값을 전달하지 않거나 정확하지 않은 값을 전달하는 경우"),
+ KOE303(
+ BAD_REQUEST,
+ "KAKAO_KOE303",
+ "invalid_grant",
+ "인가 코드 요청 시 사용한 redirect_uri와 액세스 토큰 요청 시 사용한 redirect_uri가 다른 경우"),
+ KOE319(BAD_REQUEST, "KAKAO_KOE319", "invalid_grant", "토큰 갱신 요청 시 리프레시 토큰을 전달하지 않는 경우"),
+ KOE320(
+ BAD_REQUEST,
+ "KAKAO_KOE320",
+ "invalid_grant",
+ "동일한 인가 코드를 두 번 이상 사용하거나, 이미 만료된 인가 코드를 사용한 경우, 혹은 인가 코드를 찾을 수 없는 경우"),
+ KOE322(
+ BAD_REQUEST,
+ "KAKAO_KOE322",
+ "invalid_grant",
+ "refresh_token을 찾을 수 없거나 이미 만료된 리프레시 토큰을 사용한 경우"),
+ KOE_INVALID_REQUEST(BAD_REQUEST, "KAKAO_KOE_INVALID_REQUEST", "invalid_request", "잘못된 요청인 경우"),
+ KOE400(BAD_REQUEST, "KAKAO_KOE400", "invalid_token", "ID 토큰 값이 전달되지 않았거나 올바른 형식이 아닌 ID 토큰인 경우"),
+ KOE401(BAD_REQUEST, "KAKAO_KOE401", "invalid_token", "ID 토큰을 발급한 인증 기관(iss)이 카카오 인증 서버"),
+ KOE402(BAD_REQUEST, "KAKAO_KOE402", "invalid_token", "서명이 올바르지 않아 유효한 ID 토큰이 아닌 경우"),
+ KOE403(BAD_REQUEST, "KAKAO_KOE403", "invalid_token", "만료된 ID 토큰인 경우");
+
+ private Integer status;
+ private String errorCode;
+ private String error;
+ private String reason;
+
+ @Override
+ public ErrorReason getErrorReason() {
+ return ErrorReason.builder().status(status).code(errorCode).reason(reason).build();
+ }
+
+ @Override
+ public String getExplainError() throws NoSuchFieldException {
+ Field field = this.getClass().getField(this.name());
+ ExplainError annotation = field.getAnnotation(ExplainError.class);
+ return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
+ }
+}
+
diff --git a/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java b/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java
index 411fb64..8aec9d5 100644
--- a/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java
+++ b/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java
@@ -7,19 +7,21 @@
import java.util.Collections;
import java.util.Date;
-import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
-import org.springframework.web.server.ResponseStatusException;
+import capstone.relation.api.auth.exception.AuthErrorCode;
+import capstone.relation.api.auth.exception.AuthException;
import capstone.relation.api.auth.jwt.refreshtoken.CollectionRefreshTokenRepository;
import capstone.relation.api.auth.jwt.refreshtoken.RefreshToken;
import capstone.relation.api.auth.jwt.refreshtoken.RefreshTokenRepository;
import capstone.relation.api.auth.jwt.response.TokenResponse;
import capstone.relation.user.domain.Role;
import capstone.relation.user.domain.User;
+import capstone.relation.workspace.exception.WorkSpaceErrorCode;
+import capstone.relation.workspace.exception.WorkSpaceException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
@@ -83,7 +85,7 @@ public boolean validateToken(String accessToken) {
public String generateAccessTokenByRefreshToken(String refreshTokenKey) {
long now = (new Date().getTime());
RefreshToken refreshToken = refreshTokenRepository.findByKey(refreshTokenKey)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, "유효하지 않은 리프레시 토큰 입니다."));
+ .orElseThrow(() -> new AuthException(AuthErrorCode.INVALID_TOKEN));
User user = refreshToken.user();
Date accessTokenExpiredDate = new Date(now + jwtProperties.getAccessTokenExpireTime());
return generateAccessToken(user, accessTokenExpiredDate);
@@ -116,9 +118,9 @@ public String getWorkSpaceIdByInviteCode(String inviteCode) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(inviteToken).getBody();
return claims.getSubject();
} catch (ExpiredJwtException e) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "만료된 초대 코드입니다.");
+ throw new WorkSpaceException(WorkSpaceErrorCode.EXPIRED_INVITE_CODE);
} catch (Exception e) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "유효하지 않은 초대 코드입니다.");
+ throw new WorkSpaceException(WorkSpaceErrorCode.INVALID_INVITE_CODE);
}
}
@@ -143,7 +145,7 @@ private Claims decodeAccessToken(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException expiredJwtException) {
- throw new IllegalStateException("만료된 토큰입니다.");
+ throw new AuthException(AuthErrorCode.TOKEN_EXPIRED);
}
}
}
diff --git a/src/main/java/capstone/relation/api/auth/service/AuthService.java b/src/main/java/capstone/relation/api/auth/service/AuthService.java
index 215ac23..cb15bed 100644
--- a/src/main/java/capstone/relation/api/auth/service/AuthService.java
+++ b/src/main/java/capstone/relation/api/auth/service/AuthService.java
@@ -4,11 +4,9 @@
import java.util.Map;
import java.util.Optional;
-import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.server.ResponseStatusException;
import capstone.relation.api.auth.AuthProvider;
import capstone.relation.api.auth.jwt.TokenProvider;
@@ -17,6 +15,8 @@
import capstone.relation.api.auth.jwt.response.WorkspaceStateType;
import capstone.relation.api.auth.oauth.provider.OAuthUserProvider;
import capstone.relation.user.domain.User;
+import capstone.relation.user.exception.UserErrorCode;
+import capstone.relation.user.exception.UserException;
import capstone.relation.user.repository.UserRepository;
import capstone.relation.workspace.WorkSpace;
import capstone.relation.workspace.service.InvitationService;
@@ -51,13 +51,13 @@ public TokenResponse login(AuthProvider authProvider, String accessToken) {
public TokenResponse loginWithCode(AuthProvider authProvider, String code, String inviteCode) {
String accessToken = getToken(authProvider, code);
TokenResponse response = login(authProvider, accessToken);
- if (inviteCode == null || inviteCode.isEmpty() || inviteCode.isBlank() || inviteCode.equals("null")) {
+ if (inviteCode == null || inviteCode.isEmpty() || inviteCode.isBlank() || inviteCode.equals("null"))
return response;
- }
+
Optional userOpt = userRepository.findById(response.getMemberId());
- if (userOpt.isEmpty()) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found.");
- }
+ if (userOpt.isEmpty())
+ throw new UserException(UserErrorCode.USER_NOT_FOUND);
+
WorkSpace workSpace = invitationService.getWorkSpace(inviteCode);
User user = userOpt.get();
if (user.getWorkSpace() != null) {
diff --git a/src/main/java/capstone/relation/global/annotation/ApiErrorCodeExample.java b/src/main/java/capstone/relation/global/annotation/ApiErrorCodeExample.java
new file mode 100644
index 0000000..1316b3b
--- /dev/null
+++ b/src/main/java/capstone/relation/global/annotation/ApiErrorCodeExample.java
@@ -0,0 +1,14 @@
+package capstone.relation.global.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import capstone.relation.global.exception.BaseErrorCode;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiErrorCodeExample {
+ Class extends BaseErrorCode> value();
+}
diff --git a/src/main/java/capstone/relation/global/annotation/ApiErrorExceptionsExample.java b/src/main/java/capstone/relation/global/annotation/ApiErrorExceptionsExample.java
new file mode 100644
index 0000000..5e6f63c
--- /dev/null
+++ b/src/main/java/capstone/relation/global/annotation/ApiErrorExceptionsExample.java
@@ -0,0 +1,14 @@
+package capstone.relation.global.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import capstone.relation.global.interfaces.SwaggerExampleExceptions;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiErrorExceptionsExample {
+ Class extends SwaggerExampleExceptions> value();
+}
\ No newline at end of file
diff --git a/src/main/java/capstone/relation/global/annotation/DevelopOnlyApi.java b/src/main/java/capstone/relation/global/annotation/DevelopOnlyApi.java
new file mode 100644
index 0000000..92ad515
--- /dev/null
+++ b/src/main/java/capstone/relation/global/annotation/DevelopOnlyApi.java
@@ -0,0 +1,11 @@
+package capstone.relation.global.annotation;
+
+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/capstone/relation/global/annotation/ExceptionDoc.java b/src/main/java/capstone/relation/global/annotation/ExceptionDoc.java
new file mode 100644
index 0000000..ac356a8
--- /dev/null
+++ b/src/main/java/capstone/relation/global/annotation/ExceptionDoc.java
@@ -0,0 +1,19 @@
+package capstone.relation.global.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.stereotype.Component;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface ExceptionDoc {
+ @AliasFor(annotation = Component.class)
+ String value() default "";
+}
\ No newline at end of file
diff --git a/src/main/java/capstone/relation/global/annotation/ExplainError.java b/src/main/java/capstone/relation/global/annotation/ExplainError.java
new file mode 100644
index 0000000..3d79c62
--- /dev/null
+++ b/src/main/java/capstone/relation/global/annotation/ExplainError.java
@@ -0,0 +1,17 @@
+package capstone.relation.global.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.stereotype.Component;
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface ExplainError {
+ String value() default "";
+}
diff --git a/src/main/java/capstone/relation/global/aop/ApiBlockingAspect.java b/src/main/java/capstone/relation/global/aop/ApiBlockingAspect.java
new file mode 100644
index 0000000..30a0de6
--- /dev/null
+++ b/src/main/java/capstone/relation/global/aop/ApiBlockingAspect.java
@@ -0,0 +1,28 @@
+package capstone.relation.global.aop;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+
+import capstone.relation.global.exception.GlobalDynamicException;
+import capstone.relation.global.helper.SpringEnvironmentHelper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Aspect
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class ApiBlockingAspect {
+
+ private final SpringEnvironmentHelper springEnvironmentHelper;
+
+ @Around("@annotation(capstone.relation.global.annotation.DevelopOnlyApi)")
+ public Object checkApiAcceptingCondition(ProceedingJoinPoint joinPoint) throws Throwable {
+ if (springEnvironmentHelper.isProdProfile()) {
+ throw new GlobalDynamicException(405, "Blocked Api", "not working api in production");
+ }
+ return joinPoint.proceed();
+ }
+}
diff --git a/src/main/java/capstone/relation/global/config/ExampleHolder.java b/src/main/java/capstone/relation/global/config/ExampleHolder.java
new file mode 100644
index 0000000..721d718
--- /dev/null
+++ b/src/main/java/capstone/relation/global/config/ExampleHolder.java
@@ -0,0 +1,13 @@
+package capstone.relation.global.config;
+
+import io.swagger.v3.oas.models.examples.Example;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class ExampleHolder {
+ private Example holder;
+ private String name;
+ private int code;
+}
diff --git a/src/main/java/capstone/relation/global/config/SwaggerConfig.java b/src/main/java/capstone/relation/global/config/SwaggerConfig.java
index d0c7268..9c43930 100644
--- a/src/main/java/capstone/relation/global/config/SwaggerConfig.java
+++ b/src/main/java/capstone/relation/global/config/SwaggerConfig.java
@@ -1,17 +1,47 @@
package capstone.relation.global.config;
+import static java.util.stream.Collectors.*;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.springdoc.core.customizers.OperationCustomizer;
+import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.HandlerMethod;
+import capstone.relation.global.annotation.ApiErrorCodeExample;
+import capstone.relation.global.annotation.ApiErrorExceptionsExample;
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.dto.ErrorReason;
+import capstone.relation.global.dto.ErrorResponse;
+import capstone.relation.global.exception.BaseErrorCode;
+import capstone.relation.global.exception.GlobalCodeException;
+import io.swagger.v3.oas.annotations.tags.Tag;
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 lombok.RequiredArgsConstructor;
@Configuration
+@RequiredArgsConstructor
public class SwaggerConfig {
+ private final ApplicationContext applicationContext;
@Bean
public OpenAPI openAPI() {
@@ -40,5 +70,155 @@ public OpenAPI openAPI() {
.addSecurityItem(securityRequirement)
.components(components);
}
+
+ /**
+ * Swagger 문서의 각 API 동작(Operation)을 사용자 정의하기 위해 사용
+ * ApiErrorCodeExample 어노테이션을 찾습니다. 이 어노테이션은 특정 메서드에 정의된 에러 코드 예시를 설정하는 데 사용됩니다.
+ */
+ @Bean
+ public OperationCustomizer customize() {
+ return (Operation operation, HandlerMethod handlerMethod) -> {
+ ApiErrorCodeExample apiErrorCodeExample =
+ handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
+ ApiErrorExceptionsExample apiErrorExceptionsExample =
+ handlerMethod.getMethodAnnotation(ApiErrorExceptionsExample.class);
+ List tags = getTags(handlerMethod);
+
+ // 태그 중복 설정시 제일 구체적인 값만 태그로 설정
+ if (!tags.isEmpty())
+ operation.setTags(Collections.singletonList(tags.get(0)));
+ // ApiErrorExceptionsExample 어노테이션 단 메소드 적용
+ if (apiErrorExceptionsExample != null) {
+ generateExceptionResponseExample(operation, apiErrorExceptionsExample.value());
+ }
+ // ApiErrorCodeExample 어노테이션 단 메소드 적용
+ if (apiErrorCodeExample != null)
+ generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
+
+ return operation;
+ };
+ }
+
+ /**
+ * BaseErrorCode 타입의 이넘값들을 문서화 시킵니다. ExplainError 어노테이션으로 부가설명을 붙일수있습니다. 필드들을 가져와서 예시 에러 객체를
+ * 동적으로 생성해서 예시값으로 붙입니다.
+ */
+ private void generateErrorCodeResponseExample(
+ Operation operation, Class extends BaseErrorCode> type) {
+ ApiResponses responses = operation.getResponses();
+
+ BaseErrorCode[] errorCodes = type.getEnumConstants();
+
+ Map> statusWithExampleHolders =
+ Arrays.stream(errorCodes)
+ .map(
+ baseErrorCode -> {
+ try {
+ ErrorReason errorReason = baseErrorCode.getErrorReason();
+ return ExampleHolder.builder()
+ .holder(
+ getSwaggerExample(
+ baseErrorCode.getExplainError(),
+ errorReason))
+ .code(errorReason.getStatus())
+ .name(errorReason.getCode())
+ .build();
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(groupingBy(ExampleHolder::getCode));
+
+ addExamplesToResponses(responses, statusWithExampleHolders);
+ }
+
+ /**
+ * SwaggerExampleExceptions 타입의 클래스를 문서화 시킵니다. SwaggerExampleExceptions 타입의 클래스는 필드로
+ * GlobalCodeException 타입을 가지며, GlobalCodeException 의 errorReason 와,ExplainError 의 설명을
+ * 문서화시킵니다.
+ */
+ private void generateExceptionResponseExample(Operation operation, Class> type) {
+ ApiResponses responses = operation.getResponses();
+
+ // ----------------생성
+ Object bean = applicationContext.getBean(type);
+ Field[] declaredFields = bean.getClass().getDeclaredFields();
+ Map> statusWithExampleHolders =
+ Arrays.stream(declaredFields)
+ .filter(field -> field.getAnnotation(ExplainError.class) != null)
+ .filter(field -> field.getType() == GlobalCodeException.class)
+ .map(
+ field -> {
+ try {
+ GlobalCodeException exception =
+ (GlobalCodeException)field.get(bean);
+ ExplainError annotation =
+ field.getAnnotation(ExplainError.class);
+ String value = annotation.value();
+ ErrorReason errorReason = exception.getErrorReason();
+ return ExampleHolder.builder()
+ .holder(getSwaggerExample(value, errorReason))
+ .code(errorReason.getStatus())
+ .name(field.getName())
+ .build();
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(groupingBy(ExampleHolder::getCode));
+
+ // -------------------------- 콘텐츠 세팅 코드별로 진행
+ addExamplesToResponses(responses, statusWithExampleHolders);
+ }
+
+ /**
+ * : 주어진 ErrorReason을 사용하여 ErrorResponse 객체를 생성합니다. 이 객체는 예시 응답에 포함됩니다.
+ */
+ private Example getSwaggerExample(String value, ErrorReason errorReason) {
+ ErrorResponse errorResponse = new ErrorResponse(errorReason, "요청시 패스정보입니다.");
+ Example example = new Example();
+ example.description(value);
+ example.setValue(errorResponse);
+ return example;
+ }
+
+ /**
+ * 상태 코드별로 예시를 API 응답에 추가합니다.
+ */
+ 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.getHolder());
+ });
+ content.addMediaType("application/json", mediaType);
+ apiResponse.setContent(content);
+ responses.addApiResponse(status.toString(), apiResponse);
+ });
+ }
+
+ /**
+ * API 메서드 및 클래스에 정의된 Tag 어노테이션을 가져와 태그 이름을 추출하고 리스트에 추가 합니다.
+ */
+ private static List getTags(HandlerMethod handlerMethod) {
+ List tags = new ArrayList<>();
+
+ Tag[] methodTags = handlerMethod.getMethod().getAnnotationsByType(Tag.class);
+ List methodTagStrings =
+ Arrays.stream(methodTags).map(Tag::name).collect(Collectors.toList());
+
+ Tag[] classTags = handlerMethod.getClass().getAnnotationsByType(Tag.class);
+ List classTagStrings =
+ Arrays.stream(classTags).map(Tag::name).collect(Collectors.toList());
+ tags.addAll(methodTagStrings);
+ tags.addAll(classTagStrings);
+ return tags;
+ }
}
diff --git a/src/main/java/capstone/relation/global/consts/JoEunStatic.java b/src/main/java/capstone/relation/global/consts/JoEunStatic.java
new file mode 100644
index 0000000..a55a904
--- /dev/null
+++ b/src/main/java/capstone/relation/global/consts/JoEunStatic.java
@@ -0,0 +1,29 @@
+package capstone.relation.global.consts;
+
+public class JoEunStatic {
+ public static final String AUTH_HEADER = "Authorization";
+ public static final String BEARER = "Bearer ";
+ public static final String ACCESS_TOKEN = "ACCESS_TOKEN";
+ public static final String REFRESH_TOKEN = "REFRESH_TOKEN";
+
+ public static final int MILLI_TO_SECOND = 1000;
+ public static final int BAD_REQUEST = 400;
+ public static final int UNAUTHORIZED = 401;
+ public static final int FORBIDDEN = 403;
+ public static final int NOT_FOUND = 404;
+
+ public static final int CONFLICT = 409;
+ public static final int INTERNAL_SERVER = 500;
+
+ public static final Long NO_START_NUMBER = 1000000L;
+ public static final Long MINIMUM_PAYMENT_WON = 1000L;
+ public static final Long ZERO = 0L;
+
+ public static final String KAKAO_OAUTH_QUERY_STRING =
+ "/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code";
+
+ public static final String[] SwaggerPatterns = {
+ "/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs",
+ };
+}
+
diff --git a/src/main/java/capstone/relation/global/document/ExampleController.java b/src/main/java/capstone/relation/global/document/ExampleController.java
new file mode 100644
index 0000000..c9123fd
--- /dev/null
+++ b/src/main/java/capstone/relation/global/document/ExampleController.java
@@ -0,0 +1,71 @@
+package capstone.relation.global.document;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import capstone.relation.api.auth.exception.AuthErrorCode;
+import capstone.relation.global.annotation.ApiErrorCodeExample;
+import capstone.relation.global.annotation.DevelopOnlyApi;
+import capstone.relation.global.exception.GlobalErrorCode;
+import capstone.relation.meeting.exception.MeetingErrorCode;
+import capstone.relation.user.exception.UserErrorCode;
+import capstone.relation.workspace.exception.WorkSpaceErrorCode;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping("/v1/example")
+@RequiredArgsConstructor
+@Tag(name = "Exception Document", description = "예제 에러코드 문서화")
+public class ExampleController {
+ @GetMapping("/global")
+ @DevelopOnlyApi
+ @ApiErrorCodeExample(GlobalErrorCode.class)
+ @Operation(summary = "글로벌 (aop, 서버 내부 오류등) 관련 에러 코드 나열")
+ public void example() {
+ }
+
+ @GetMapping("/auth")
+ @DevelopOnlyApi
+ @Operation(summary = "인증 도메인 관련 에러 코드 나열")
+ @ApiErrorCodeExample(AuthErrorCode.class)
+ public void getAuthErrorCode() {
+ }
+
+ @GetMapping("/user")
+ @DevelopOnlyApi
+ @Operation(summary = "유저 도메인 관련 에러 코드 나열")
+ @ApiErrorCodeExample(UserErrorCode.class)
+ public void getUserErrorCode() {
+ }
+
+ @GetMapping("/workspace")
+ @DevelopOnlyApi
+ @Operation(summary = "워크스페이스 도메인 관련 에러 코드 나열")
+ @ApiErrorCodeExample(WorkSpaceErrorCode.class)
+ public void getWorkSpaceErrorCode() {
+ }
+
+ @GetMapping("/meeting")
+ @DevelopOnlyApi
+ @Operation(summary = "화상회의 도메인 관련 에러 코드 나열")
+ @ApiErrorCodeExample(MeetingErrorCode.class)
+ public void getMeetingErrorCode() {
+ }
+
+ // @GetMapping("/chat")
+ // @DevelopOnlyApi
+ // @Operation(summary = "채팅 도메인 관련 에러 코드 나열 (이건 소켓으로 처리됩니다.")
+ // @ApiErrorCodeExample(ChatErrorCode.class)
+ // public void getChatErrorCode() {
+ // }
+
+ // @GetMapping("/socket")
+ // @DevelopOnlyApi
+ // @Operation(summary = "소켓 도메인 관련 에러 코드 나열")
+ // @ApiErrorCodeExample(SocketErrorCode.class)
+ // public void getSocketErrorCode() {
+ // }
+}
diff --git a/src/main/java/capstone/relation/global/dto/ErrorReason.java b/src/main/java/capstone/relation/global/dto/ErrorReason.java
new file mode 100644
index 0000000..4b453dd
--- /dev/null
+++ b/src/main/java/capstone/relation/global/dto/ErrorReason.java
@@ -0,0 +1,13 @@
+package capstone.relation.global.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class ErrorReason {
+ private final Integer status;
+ private final String code;
+ private final String reason;
+}
+
diff --git a/src/main/java/capstone/relation/global/dto/ErrorResponse.java b/src/main/java/capstone/relation/global/dto/ErrorResponse.java
new file mode 100644
index 0000000..f16c08d
--- /dev/null
+++ b/src/main/java/capstone/relation/global/dto/ErrorResponse.java
@@ -0,0 +1,33 @@
+package capstone.relation.global.dto;
+
+import java.time.LocalDateTime;
+
+import lombok.Getter;
+
+@Getter
+public class ErrorResponse {
+
+ private final boolean success = false;
+ private final int status;
+ private final String code;
+ private final String reason;
+ private final LocalDateTime timeStamp;
+
+ private final String path;
+
+ public ErrorResponse(ErrorReason errorReason, String path) {
+ this.status = errorReason.getStatus();
+ this.code = errorReason.getCode();
+ this.reason = errorReason.getReason();
+ this.timeStamp = LocalDateTime.now();
+ this.path = path;
+ }
+
+ public ErrorResponse(int status, String code, String reason, String path) {
+ this.status = status;
+ this.code = code;
+ this.reason = reason;
+ this.timeStamp = LocalDateTime.now();
+ this.path = path;
+ }
+}
diff --git a/src/main/java/capstone/relation/global/exception/BaseErrorCode.java b/src/main/java/capstone/relation/global/exception/BaseErrorCode.java
new file mode 100644
index 0000000..015b41a
--- /dev/null
+++ b/src/main/java/capstone/relation/global/exception/BaseErrorCode.java
@@ -0,0 +1,9 @@
+package capstone.relation.global.exception;
+
+import capstone.relation.global.dto.ErrorReason;
+
+public interface BaseErrorCode {
+ public ErrorReason getErrorReason();
+
+ String getExplainError() throws NoSuchFieldException;
+}
diff --git a/src/main/java/capstone/relation/global/exception/GlobalCodeException.java b/src/main/java/capstone/relation/global/exception/GlobalCodeException.java
new file mode 100644
index 0000000..ce7d420
--- /dev/null
+++ b/src/main/java/capstone/relation/global/exception/GlobalCodeException.java
@@ -0,0 +1,15 @@
+package capstone.relation.global.exception;
+
+import capstone.relation.global.dto.ErrorReason;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class GlobalCodeException extends RuntimeException {
+ private BaseErrorCode errorCode;
+
+ public ErrorReason getErrorReason() {
+ return this.errorCode.getErrorReason();
+ }
+}
diff --git a/src/main/java/capstone/relation/global/exception/GlobalDynamicException.java b/src/main/java/capstone/relation/global/exception/GlobalDynamicException.java
new file mode 100644
index 0000000..6d02536
--- /dev/null
+++ b/src/main/java/capstone/relation/global/exception/GlobalDynamicException.java
@@ -0,0 +1,12 @@
+package capstone.relation.global.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class GlobalDynamicException extends RuntimeException {
+ private final int status;
+ private final String code;
+ private final String reason;
+}
\ No newline at end of file
diff --git a/src/main/java/capstone/relation/global/exception/GlobalErrorCode.java b/src/main/java/capstone/relation/global/exception/GlobalErrorCode.java
new file mode 100644
index 0000000..20d5735
--- /dev/null
+++ b/src/main/java/capstone/relation/global/exception/GlobalErrorCode.java
@@ -0,0 +1,56 @@
+package capstone.relation.global.exception;
+
+import static capstone.relation.global.consts.JoEunStatic.*;
+
+import java.lang.reflect.Field;
+import java.util.Objects;
+
+import capstone.relation.global.annotation.ExplainError;
+import capstone.relation.global.dto.ErrorReason;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 글로벌 관련 예외 코드들이 나온 곳입니다. 인증 , global, aop 종류등 도메인 제외한 exception 코드들이 모이는 곳입니다. 도메인 관련 Exception
+ * code 들은 도메인 내부 exception 패키지에 위치시키면 됩니다.
+ */
+@Getter
+@AllArgsConstructor
+public enum GlobalErrorCode implements BaseErrorCode {
+ @ExplainError("백엔드에서 예시로만든 에러입니다. 개발용")
+ EXAMPLE_NOT_FOUND(NOT_FOUND, "EXAMPLE_404_1", "예시를 찾을 수 없는 오류입니다."),
+
+ @ExplainError("밸리데이션 (검증 과정 수행속 ) 발생하는 오류입니다.")
+ ARGUMENT_NOT_VALID_ERROR(BAD_REQUEST, "GLOBAL_400_1", "검증 오류"),
+
+ @ExplainError("500번대 알수없는 오류입니다. 서버 관리자에게 문의 주세요")
+ INTERNAL_SERVER_ERROR(INTERNAL_SERVER, "GLOBAL_500_1", "서버 오류. 관리자에게 문의 부탁드립니다."),
+
+ OTHER_SERVER_BAD_REQUEST(BAD_REQUEST, "FEIGN_400_1", "다른 서버로 한 요청이 Bad Request 입니다."),
+ OTHER_SERVER_UNAUTHORIZED(BAD_REQUEST, "FEIGN_400_2", "다른 서버로 한 요청이 Unauthorized 입니다."),
+ OTHER_SERVER_FORBIDDEN(BAD_REQUEST, "FEIGN_400_3", "다른 서버로 한 요청이 Forbidden 입니다."),
+ OTHER_SERVER_EXPIRED_TOKEN(BAD_REQUEST, "FEIGN_400_4", "다른 서버로 한 요청이 Expired Token 입니다."),
+ OTHER_SERVER_NOT_FOUND(BAD_REQUEST, "FEIGN_400_5", "다른 서버로 한 요청이 Not Found 입니다."),
+ OTHER_SERVER_INTERNAL_SERVER_ERROR(
+ BAD_REQUEST, "FEIGN_400_6", "다른 서버로 한 요청이 Internal Server Error 입니다."),
+ SECURITY_CONTEXT_NOT_FOUND(500, "GLOBAL_500_2", "SecurityContext를 찾을 수 없습니다."),
+
+ BAD_FILE_EXTENSION(BAD_REQUEST, "FILE_400_1", "파일 확장자가 잘못 되었습니다."),
+ TOO_MANY_REQUEST(429, "GLOBAL_429_1", "과도한 요청을 보내셨습니다. 잠시 기다려 주세요.");
+ private Integer status;
+ private String code;
+ private String reason;
+
+ @Override
+ public ErrorReason getErrorReason() {
+ return ErrorReason.builder().reason(reason).code(code).status(status).build();
+ }
+
+ @Override
+ public String getExplainError() throws NoSuchFieldException {
+ Field field = this.getClass().getField(this.name());
+ ExplainError annotation = field.getAnnotation(ExplainError.class);
+ return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
+ }
+}
+
diff --git a/src/main/java/capstone/relation/global/exception/GlobalException.java b/src/main/java/capstone/relation/global/exception/GlobalException.java
new file mode 100644
index 0000000..acab44d
--- /dev/null
+++ b/src/main/java/capstone/relation/global/exception/GlobalException.java
@@ -0,0 +1,7 @@
+package capstone.relation.global.exception;
+
+public class GlobalException extends GlobalCodeException {
+ public GlobalException(GlobalErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/capstone/relation/global/exception/GlobalExceptionHandler.java b/src/main/java/capstone/relation/global/exception/GlobalExceptionHandler.java
index ce12c7e..fc05b61 100644
--- a/src/main/java/capstone/relation/global/exception/GlobalExceptionHandler.java
+++ b/src/main/java/capstone/relation/global/exception/GlobalExceptionHandler.java
@@ -1,38 +1,162 @@
package capstone.relation.global.exception;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
-import org.springframework.http.ProblemDetail;
+import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+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.server.ResponseStatusException;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import capstone.relation.global.dto.ErrorReason;
+import capstone.relation.global.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolationException;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
@RestControllerAdvice
-public class GlobalExceptionHandler {
+@Slf4j
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+ //TODO: 디코 알림 보내는 거 추가
- @ExceptionHandler(ResponseStatusException.class) //커스텀 예외처리
- public ResponseEntity handleResponseStatusException(ResponseStatusException ex, WebRequest request) {
- return new ResponseEntity<>(ex.getReason(), ex.getStatusCode());
+ // ResponseEntityExceptionHandler를 상속받아서 예외처리를 할 수 있다.
+ @Override
+ protected ResponseEntity