diff --git a/backend/src/main/java/org/donggle/backend/application/service/AuthService.java b/backend/src/main/java/org/donggle/backend/application/service/AuthService.java index 49254ced4..307b5e422 100644 --- a/backend/src/main/java/org/donggle/backend/application/service/AuthService.java +++ b/backend/src/main/java/org/donggle/backend/application/service/AuthService.java @@ -6,9 +6,9 @@ import org.donggle.backend.application.service.oauth.kakao.dto.KakaoProfileResponse; import org.donggle.backend.auth.JwtTokenProvider; import org.donggle.backend.auth.JwtTokenService; -import org.donggle.backend.auth.exception.NoSuchTokenException; import org.donggle.backend.domain.member.Member; import org.donggle.backend.domain.member.MemberName; +import org.donggle.backend.exception.notfound.MemberNotFoundException; import org.donggle.backend.ui.response.TokenResponse; import org.springframework.stereotype.Service; @@ -19,7 +19,7 @@ public class AuthService { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; private final JwtTokenService jwtTokenService; - + public TokenResponse loginByKakao(final KakaoProfileResponse kakaoProfileResponse) { final Member loginMember = memberRepository.findByKakaoId(kakaoProfileResponse.id()) .orElseGet(() -> memberRepository.save(Member.createByKakao( @@ -28,20 +28,20 @@ public TokenResponse loginByKakao(final KakaoProfileResponse kakaoProfileRespons )); return createTokens(loginMember); } - + public TokenResponse reissueAccessTokenAndRefreshToken(final Long memberId) { final Member member = memberRepository.findById(memberId). - orElseThrow(NoSuchTokenException::new); - + orElseThrow(() -> new MemberNotFoundException(memberId)); + return createTokens(member); } - + private TokenResponse createTokens(final Member loginMember) { final String accessToken = jwtTokenProvider.createAccessToken(loginMember.getId()); final String refreshToken = jwtTokenProvider.createRefreshToken(loginMember.getId()); - + jwtTokenService.synchronizeRefreshToken(loginMember, refreshToken); - + return new TokenResponse(accessToken, refreshToken); } } diff --git a/backend/src/main/java/org/donggle/backend/application/service/CategoryService.java b/backend/src/main/java/org/donggle/backend/application/service/CategoryService.java index 099bc4ffc..8cb658c84 100644 --- a/backend/src/main/java/org/donggle/backend/application/service/CategoryService.java +++ b/backend/src/main/java/org/donggle/backend/application/service/CategoryService.java @@ -184,7 +184,7 @@ private void addCategoryOrder(final Long nextCategoryId, final Category category private void validateBasicCategory(final Category category) { if (category.isBasic()) { - throw new InvalidBasicCategoryException(); + throw new InvalidBasicCategoryException(category.getId()); } } diff --git a/backend/src/main/java/org/donggle/backend/application/service/WritingService.java b/backend/src/main/java/org/donggle/backend/application/service/WritingService.java index c05521ee4..868671f2e 100644 --- a/backend/src/main/java/org/donggle/backend/application/service/WritingService.java +++ b/backend/src/main/java/org/donggle/backend/application/service/WritingService.java @@ -63,7 +63,7 @@ public Long uploadMarkDownFile(final Long memberId, final MarkdownUploadRequest //TODO: member checking final String originalFilename = request.file().getOriginalFilename(); if (!Objects.requireNonNull(originalFilename).endsWith(MD_FORMAT)) { - throw new InvalidFileFormatException(); + throw new InvalidFileFormatException(originalFilename); } final String originalFileText = new String(request.file().getBytes(), StandardCharsets.UTF_8); diff --git a/backend/src/main/java/org/donggle/backend/auth/JwtTokenProvider.java b/backend/src/main/java/org/donggle/backend/auth/JwtTokenProvider.java index 70b5277b9..f5ec4a879 100644 --- a/backend/src/main/java/org/donggle/backend/auth/JwtTokenProvider.java +++ b/backend/src/main/java/org/donggle/backend/auth/JwtTokenProvider.java @@ -7,7 +7,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import org.donggle.backend.auth.exception.NoSuchTokenException; +import org.donggle.backend.auth.exception.ExpiredAccessTokenException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -52,7 +52,9 @@ private String createToken(final Long payload, final long validityInMilliseconds } public Long getPayload(final String token) { - return getClaims(token).getBody().get(MEMBER_ID_KEY, Long.class); + return getClaims(token) + .getBody() + .get(MEMBER_ID_KEY, Long.class); } public boolean inValidTokenUsage(final String token) { @@ -60,13 +62,16 @@ public boolean inValidTokenUsage(final String token) { final Jws claims = getClaims(token); return claims.getBody().getExpiration().before(new Date()); } catch (final ExpiredJwtException e) { - throw new NoSuchTokenException(); + throw new ExpiredAccessTokenException(); } catch (final JwtException | IllegalArgumentException e) { return true; } } private Jws getClaims(final String token) { - return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); } } diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/AuthorizationHeaderNotFoundException.java b/backend/src/main/java/org/donggle/backend/auth/exception/AuthorizationHeaderNotFoundException.java new file mode 100644 index 000000000..a25461f55 --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/AuthorizationHeaderNotFoundException.java @@ -0,0 +1,18 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class AuthorizationHeaderNotFoundException extends AuthenticationException { + public AuthorizationHeaderNotFoundException() { + super(null); + } + + public AuthorizationHeaderNotFoundException(final Throwable cause) { + super(null, cause); + } + + @Override + public String getHint() { + return "Authorization 해더값이 존재하지 않습니다."; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/EmptyAuthorizationHeaderException.java b/backend/src/main/java/org/donggle/backend/auth/exception/EmptyAuthorizationHeaderException.java deleted file mode 100644 index 16827e98e..000000000 --- a/backend/src/main/java/org/donggle/backend/auth/exception/EmptyAuthorizationHeaderException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.donggle.backend.auth.exception; - -import org.donggle.backend.exception.business.BusinessException; - -public class EmptyAuthorizationHeaderException extends BusinessException { - private static final String MESSAGE = "header에 Authorization이 존재하지 않습니다."; - - public EmptyAuthorizationHeaderException() { - super(MESSAGE); - } -} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/ExpiredAccessTokenException.java b/backend/src/main/java/org/donggle/backend/auth/exception/ExpiredAccessTokenException.java new file mode 100644 index 000000000..6fca77ec9 --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/ExpiredAccessTokenException.java @@ -0,0 +1,20 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class ExpiredAccessTokenException extends AuthenticationException { + private static final String MESSAGE = "유효하지 않은 토큰입니다."; + + public ExpiredAccessTokenException() { + super(MESSAGE); + } + + public ExpiredAccessTokenException(final Throwable cause) { + super(MESSAGE, cause); + } + + @Override + public String getHint() { + return "AccessToken이 만료되었습니다. RefreshToken값을 요청하세요."; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/ExpiredRefreshTokenException.java b/backend/src/main/java/org/donggle/backend/auth/exception/ExpiredRefreshTokenException.java new file mode 100644 index 000000000..a9a1a711d --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/ExpiredRefreshTokenException.java @@ -0,0 +1,20 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class ExpiredRefreshTokenException extends AuthenticationException { + private static final String MESSAGE = "유효하지 않은 토큰입니다."; + + public ExpiredRefreshTokenException() { + super(MESSAGE); + } + + public ExpiredRefreshTokenException(final Throwable cause) { + super(MESSAGE, cause); + } + + @Override + public String getHint() { + return "RefreshToken이 만료되었습니다. 다시 로그인을 진행하세요."; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/InvalidAccessTokenException.java b/backend/src/main/java/org/donggle/backend/auth/exception/InvalidAccessTokenException.java deleted file mode 100644 index f4260eb3d..000000000 --- a/backend/src/main/java/org/donggle/backend/auth/exception/InvalidAccessTokenException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.donggle.backend.auth.exception; - -import org.donggle.backend.exception.business.BusinessException; - -public class InvalidAccessTokenException extends BusinessException { - private static final String MESSAGE = "유효하지 않은 토큰입니다."; - - public InvalidAccessTokenException() { - super(MESSAGE); - } -} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/InvalidAuthorizationHeaderTypeException.java b/backend/src/main/java/org/donggle/backend/auth/exception/InvalidAuthorizationHeaderTypeException.java new file mode 100644 index 000000000..b697f140a --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/InvalidAuthorizationHeaderTypeException.java @@ -0,0 +1,22 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class InvalidAuthorizationHeaderTypeException extends AuthenticationException { + private final String authorizationHeader; + + public InvalidAuthorizationHeaderTypeException(final String authorizationHeader) { + super(null); + this.authorizationHeader = authorizationHeader; + } + + public InvalidAuthorizationHeaderTypeException(final String authorizationHeader, final Throwable cause) { + super(null, cause); + this.authorizationHeader = authorizationHeader; + } + + @Override + public String getHint() { + return "Authorization 헤더의 타입이 올바르지 않습니다. 입력한 헤더: " + authorizationHeader; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/InvalidRefreshTokenException.java b/backend/src/main/java/org/donggle/backend/auth/exception/InvalidRefreshTokenException.java new file mode 100644 index 000000000..afdbb56ca --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/InvalidRefreshTokenException.java @@ -0,0 +1,18 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class InvalidRefreshTokenException extends AuthenticationException { + public InvalidRefreshTokenException() { + super(null); + } + + public InvalidRefreshTokenException(final Throwable cause) { + super(null, cause); + } + + @Override + public String getHint() { + return "유효하지 않은 RefreshToken입니다. 다시 로그인을 진행하세요."; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/NoRefreshTokenInCookieException.java b/backend/src/main/java/org/donggle/backend/auth/exception/NoRefreshTokenInCookieException.java new file mode 100644 index 000000000..2ad66d3c1 --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/NoRefreshTokenInCookieException.java @@ -0,0 +1,18 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class NoRefreshTokenInCookieException extends AuthenticationException { + public NoRefreshTokenInCookieException() { + super(null); + } + + public NoRefreshTokenInCookieException(final Throwable cause) { + super(null, cause); + } + + @Override + public String getHint() { + return "쿠키에 RefreshToken이 존재하지 않습니다."; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/NoSuchTokenException.java b/backend/src/main/java/org/donggle/backend/auth/exception/NoSuchTokenException.java deleted file mode 100644 index d6ec50c5c..000000000 --- a/backend/src/main/java/org/donggle/backend/auth/exception/NoSuchTokenException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.donggle.backend.auth.exception; - -import org.donggle.backend.exception.business.BusinessException; - -public class NoSuchTokenException extends BusinessException { - private static final String MESSAGE = "존재하지 않는 토큰입니다."; - - public NoSuchTokenException() { - super(MESSAGE); - } -} diff --git a/backend/src/main/java/org/donggle/backend/auth/exception/RefreshTokenNotFoundException.java b/backend/src/main/java/org/donggle/backend/auth/exception/RefreshTokenNotFoundException.java new file mode 100644 index 000000000..37f7bcdc4 --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/auth/exception/RefreshTokenNotFoundException.java @@ -0,0 +1,18 @@ +package org.donggle.backend.auth.exception; + +import org.donggle.backend.exception.authentication.AuthenticationException; + +public class RefreshTokenNotFoundException extends AuthenticationException { + public RefreshTokenNotFoundException() { + super(null); + } + + public RefreshTokenNotFoundException(final Throwable cause) { + super(null, cause); + } + + @Override + public String getHint() { + return "존재하지 않는 토큰입니다. 다시 로그인을 진행하세요."; + } +} diff --git a/backend/src/main/java/org/donggle/backend/auth/presentation/AuthInterceptor.java b/backend/src/main/java/org/donggle/backend/auth/presentation/AuthInterceptor.java index 9b4975651..0dd86f846 100644 --- a/backend/src/main/java/org/donggle/backend/auth/presentation/AuthInterceptor.java +++ b/backend/src/main/java/org/donggle/backend/auth/presentation/AuthInterceptor.java @@ -4,7 +4,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.donggle.backend.auth.JwtTokenProvider; -import org.donggle.backend.auth.exception.InvalidAccessTokenException; +import org.donggle.backend.auth.exception.ExpiredAccessTokenException; import org.donggle.backend.auth.support.AuthorizationExtractor; import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.HandlerInterceptor; @@ -26,7 +26,7 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp private void validateToken(final HttpServletRequest request) { final String token = AuthorizationExtractor.extract(request); if (jwtTokenProvider.inValidTokenUsage(token)) { - throw new InvalidAccessTokenException(); + throw new ExpiredAccessTokenException(); } } -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/donggle/backend/auth/presentation/RefreshTokenAuthInterceptor.java b/backend/src/main/java/org/donggle/backend/auth/presentation/RefreshTokenAuthInterceptor.java index 73e5c89a7..a5115d4eb 100644 --- a/backend/src/main/java/org/donggle/backend/auth/presentation/RefreshTokenAuthInterceptor.java +++ b/backend/src/main/java/org/donggle/backend/auth/presentation/RefreshTokenAuthInterceptor.java @@ -6,7 +6,10 @@ import org.donggle.backend.application.repository.TokenRepository; import org.donggle.backend.auth.JwtToken; import org.donggle.backend.auth.JwtTokenProvider; -import org.donggle.backend.auth.exception.NoSuchTokenException; +import org.donggle.backend.auth.exception.ExpiredRefreshTokenException; +import org.donggle.backend.auth.exception.InvalidRefreshTokenException; +import org.donggle.backend.auth.exception.NoRefreshTokenInCookieException; +import org.donggle.backend.auth.exception.RefreshTokenNotFoundException; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Arrays; @@ -22,11 +25,14 @@ public boolean preHandle(final HttpServletRequest request, final Object handler) { final String refreshToken = extract(request); final Long memberId = jwtTokenProvider.getPayload(refreshToken); - final JwtToken jwtToken = tokenRepository.findByMemberId(memberId) - .orElseThrow(NoSuchTokenException::new); + final JwtToken findRefreshToken = tokenRepository.findByMemberId(memberId) + .orElseThrow(RefreshTokenNotFoundException::new); - if (jwtToken.isDifferentRefreshToken(refreshToken) || jwtTokenProvider.inValidTokenUsage(refreshToken)) { - throw new NoSuchTokenException(); + if (findRefreshToken.isDifferentRefreshToken(refreshToken)) { + throw new InvalidRefreshTokenException(); + } + if (jwtTokenProvider.inValidTokenUsage(refreshToken)) { + throw new ExpiredRefreshTokenException(); } return true; @@ -36,7 +42,7 @@ private String extract(final HttpServletRequest request) { return Arrays.stream(request.getCookies()) .filter(cookie -> "refreshToken".equals(cookie.getName())) .findFirst() - .orElseThrow(NoSuchTokenException::new) + .orElseThrow(NoRefreshTokenInCookieException::new) .getValue(); } } diff --git a/backend/src/main/java/org/donggle/backend/auth/support/AuthorizationExtractor.java b/backend/src/main/java/org/donggle/backend/auth/support/AuthorizationExtractor.java index 8949a4fcb..2f9cd00ce 100644 --- a/backend/src/main/java/org/donggle/backend/auth/support/AuthorizationExtractor.java +++ b/backend/src/main/java/org/donggle/backend/auth/support/AuthorizationExtractor.java @@ -3,8 +3,8 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.donggle.backend.auth.exception.EmptyAuthorizationHeaderException; -import org.donggle.backend.auth.exception.NoSuchTokenException; +import org.donggle.backend.auth.exception.AuthorizationHeaderNotFoundException; +import org.donggle.backend.auth.exception.InvalidAuthorizationHeaderTypeException; import org.springframework.http.HttpHeaders; import java.util.Objects; @@ -16,7 +16,7 @@ public class AuthorizationExtractor { public static String extract(final HttpServletRequest request) { final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); if (Objects.isNull(authorizationHeader)) { - throw new EmptyAuthorizationHeaderException(); + throw new AuthorizationHeaderNotFoundException(); } validateAuthorizationFormat(authorizationHeader); @@ -25,7 +25,7 @@ public static String extract(final HttpServletRequest request) { private static void validateAuthorizationFormat(final String authorizationHeader) { if (!authorizationHeader.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) { - throw new NoSuchTokenException(); + throw new InvalidAuthorizationHeaderTypeException(authorizationHeader); } } -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/donggle/backend/exception/authentication/AuthenticationException.java b/backend/src/main/java/org/donggle/backend/exception/authentication/AuthenticationException.java index 7fe89ffa9..e8408f3a6 100644 --- a/backend/src/main/java/org/donggle/backend/exception/authentication/AuthenticationException.java +++ b/backend/src/main/java/org/donggle/backend/exception/authentication/AuthenticationException.java @@ -1,11 +1,19 @@ package org.donggle.backend.exception.authentication; +import org.springframework.http.HttpStatus; + public abstract class AuthenticationException extends RuntimeException { public AuthenticationException(final String message) { super(message); } - + public AuthenticationException(final String message, final Throwable cause) { super(message, cause); } + + public abstract String getHint(); + + public final int getErrorCode() { + return HttpStatus.UNAUTHORIZED.value(); + } } diff --git a/backend/src/main/java/org/donggle/backend/exception/business/BusinessException.java b/backend/src/main/java/org/donggle/backend/exception/business/BusinessException.java index 4f32d176f..eb7277d32 100644 --- a/backend/src/main/java/org/donggle/backend/exception/business/BusinessException.java +++ b/backend/src/main/java/org/donggle/backend/exception/business/BusinessException.java @@ -8,4 +8,8 @@ public BusinessException(final String message) { public BusinessException(final String message, final Throwable cause) { super(message, cause); } + + public abstract String getHint(); + + public abstract int getErrorCode(); } diff --git a/backend/src/main/java/org/donggle/backend/exception/business/InvalidBasicCategoryException.java b/backend/src/main/java/org/donggle/backend/exception/business/InvalidBasicCategoryException.java index 2ad78db8b..09f4474c0 100644 --- a/backend/src/main/java/org/donggle/backend/exception/business/InvalidBasicCategoryException.java +++ b/backend/src/main/java/org/donggle/backend/exception/business/InvalidBasicCategoryException.java @@ -1,13 +1,29 @@ package org.donggle.backend.exception.business; +import org.springframework.http.HttpStatus; + public class InvalidBasicCategoryException extends BusinessException { private static final String MESSAGE = "기본 카테고리는 변경이 불가합니다."; + + private final Long categoryId; - public InvalidBasicCategoryException() { + public InvalidBasicCategoryException(final Long categoryId) { super(MESSAGE); + this.categoryId = categoryId; } - public InvalidBasicCategoryException(final Throwable cause) { + public InvalidBasicCategoryException(final Long categoryId, final Throwable cause) { super(MESSAGE, cause); + this.categoryId = categoryId; + } + + @Override + public String getHint() { + return "기본 카테고리는 변경이 불가합니다. 입력한 id: " + categoryId; + } + + @Override + public int getErrorCode() { + return HttpStatus.BAD_REQUEST.value(); } } diff --git a/backend/src/main/java/org/donggle/backend/exception/business/InvalidFileFormatException.java b/backend/src/main/java/org/donggle/backend/exception/business/InvalidFileFormatException.java index ba1413aed..c77aafabb 100644 --- a/backend/src/main/java/org/donggle/backend/exception/business/InvalidFileFormatException.java +++ b/backend/src/main/java/org/donggle/backend/exception/business/InvalidFileFormatException.java @@ -1,13 +1,29 @@ package org.donggle.backend.exception.business; +import org.springframework.http.HttpStatus; + public class InvalidFileFormatException extends BusinessException { private static final String MESSAGE = "지원하지 않는 파일 형식입니다."; + + private final String originalFileName; - public InvalidFileFormatException() { + public InvalidFileFormatException(final String originalFilename) { super(MESSAGE); + this.originalFileName = originalFilename; } - public InvalidFileFormatException(final Throwable cause) { + public InvalidFileFormatException(final String originalFileName, final Throwable cause) { super(MESSAGE, cause); + this.originalFileName = originalFileName; + } + + @Override + public String getHint() { + return "해당 파일은 지원하지 않습니다. 입력한 파일 : " + originalFileName; + } + + @Override + public int getErrorCode() { + return HttpStatus.BAD_REQUEST.value(); } } diff --git a/backend/src/main/java/org/donggle/backend/exception/notfound/BlogNotFoundException.java b/backend/src/main/java/org/donggle/backend/exception/notfound/BlogNotFoundException.java index 0cd95caa8..ba6869326 100644 --- a/backend/src/main/java/org/donggle/backend/exception/notfound/BlogNotFoundException.java +++ b/backend/src/main/java/org/donggle/backend/exception/notfound/BlogNotFoundException.java @@ -1,13 +1,22 @@ package org.donggle.backend.exception.notfound; public final class BlogNotFoundException extends NotFoundException { - private static final String MESSAGE = "해당 블로그를 찾을 수 없습니다. 블로그 이름: "; + private static final String MESSAGE = "존재하지 않는 블로그입니다."; + + private final String blogName; - public BlogNotFoundException(final String name) { - super(MESSAGE + name); + public BlogNotFoundException(final String blogName) { + super(MESSAGE); + this.blogName = blogName; } - public BlogNotFoundException(final String name, final Throwable cause) { - super(MESSAGE + name, cause); + public BlogNotFoundException(final String blogName, final Throwable cause) { + super(MESSAGE, cause); + this.blogName = blogName; + } + + @Override + public String getHint() { + return "해당 블로그를 찾을 수 없습니다. 블로그 이름: " + blogName; } } diff --git a/backend/src/main/java/org/donggle/backend/exception/notfound/CategoryNotFoundException.java b/backend/src/main/java/org/donggle/backend/exception/notfound/CategoryNotFoundException.java index 145308b1f..62c34adf3 100644 --- a/backend/src/main/java/org/donggle/backend/exception/notfound/CategoryNotFoundException.java +++ b/backend/src/main/java/org/donggle/backend/exception/notfound/CategoryNotFoundException.java @@ -1,13 +1,22 @@ package org.donggle.backend.exception.notfound; -public class CategoryNotFoundException extends NotFoundException { - private static final String MESSAGE = "해당 카테고리를 찾을 수 없습니다. 입력한 id: "; +public final class CategoryNotFoundException extends NotFoundException { + private static final String MESSAGE = "존재하지 않는 카테고리 입니다."; + + private final Long categoryId; - public CategoryNotFoundException(final Long id) { - super(MESSAGE + id); + public CategoryNotFoundException(final Long categoryId) { + super(MESSAGE); + this.categoryId = categoryId; } - public CategoryNotFoundException(final Long id, final Throwable cause) { - super(MESSAGE + id, cause); + public CategoryNotFoundException(final Long categoryId, final Throwable cause) { + super(MESSAGE, cause); + this.categoryId = categoryId; + } + + @Override + public String getHint() { + return "해당 카테고리를 찾을 수 없습니다. 입력한 id: " + categoryId; } } diff --git a/backend/src/main/java/org/donggle/backend/exception/notfound/MemberNotFoundException.java b/backend/src/main/java/org/donggle/backend/exception/notfound/MemberNotFoundException.java index 1048a413e..ba4c331ea 100644 --- a/backend/src/main/java/org/donggle/backend/exception/notfound/MemberNotFoundException.java +++ b/backend/src/main/java/org/donggle/backend/exception/notfound/MemberNotFoundException.java @@ -1,13 +1,22 @@ package org.donggle.backend.exception.notfound; public final class MemberNotFoundException extends NotFoundException { - private static final String MESSAGE = "해당 사용자를 찾을 수 없습니다. 입력한 id: "; + private static final String MESSAGE = "존재하지 않는 사용자입니다."; + + private final Long memberId; - public MemberNotFoundException(final Long id) { - super(MESSAGE + id); + public MemberNotFoundException(final Long memberId) { + super(MESSAGE); + this.memberId = memberId; } - public MemberNotFoundException(final Long id, final Throwable cause) { - super(MESSAGE + id, cause); + public MemberNotFoundException(final Long memberId, final Throwable cause) { + super(MESSAGE, cause); + this.memberId = memberId; + } + + @Override + public String getHint() { + return "해당 사용자를 찾을 수 없습니다. 입력한 id: " + memberId; } } diff --git a/backend/src/main/java/org/donggle/backend/exception/notfound/NotFoundException.java b/backend/src/main/java/org/donggle/backend/exception/notfound/NotFoundException.java index affaa1cec..eef27866b 100644 --- a/backend/src/main/java/org/donggle/backend/exception/notfound/NotFoundException.java +++ b/backend/src/main/java/org/donggle/backend/exception/notfound/NotFoundException.java @@ -1,11 +1,19 @@ package org.donggle.backend.exception.notfound; +import org.springframework.http.HttpStatus; + public abstract class NotFoundException extends RuntimeException { public NotFoundException(final String message) { super(message); } - + public NotFoundException(final String message, final Throwable cause) { super(message, cause); } + + public abstract String getHint(); + + public final int getErrorCode() { + return HttpStatus.NOT_FOUND.value(); + } } diff --git a/backend/src/main/java/org/donggle/backend/exception/notfound/WritingNotFoundException.java b/backend/src/main/java/org/donggle/backend/exception/notfound/WritingNotFoundException.java index 3c2af6d83..de161f923 100644 --- a/backend/src/main/java/org/donggle/backend/exception/notfound/WritingNotFoundException.java +++ b/backend/src/main/java/org/donggle/backend/exception/notfound/WritingNotFoundException.java @@ -1,13 +1,22 @@ package org.donggle.backend.exception.notfound; public class WritingNotFoundException extends NotFoundException { - private static final String MESSAGE = "해당 글을 찾을 수 없습니다. 입력 id: "; - + private static final String MESSAGE = "존재하지 않는 글입니다."; + + private final Long writingId; + public WritingNotFoundException(final Long writingId) { - super(MESSAGE + writingId); + super(MESSAGE); + this.writingId = writingId; } - + public WritingNotFoundException(final Long writingId, final Throwable cause) { - super(MESSAGE + writingId, cause); + super(MESSAGE, cause); + this.writingId = writingId; + } + + @Override + public String getHint() { + return "해당 글을 찾을 수 없습니다. 입력 id: " + writingId; } } diff --git a/backend/src/main/java/org/donggle/backend/ui/common/ErrorContent.java b/backend/src/main/java/org/donggle/backend/ui/common/ErrorContent.java new file mode 100644 index 000000000..3d8e9b2cb --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/ui/common/ErrorContent.java @@ -0,0 +1,11 @@ +package org.donggle.backend.ui.common; + +public record ErrorContent(String message, String hint, int code){ + public static ErrorContent of(final String message, final String hint, final int code) { + return new ErrorContent(message, hint, code); + } + + public static ErrorContent of(final String hint, final int code) { + return new ErrorContent(null, hint, code); + } +} diff --git a/backend/src/main/java/org/donggle/backend/ui/common/ErrorResponse.java b/backend/src/main/java/org/donggle/backend/ui/common/ErrorResponse.java deleted file mode 100644 index c323b284e..000000000 --- a/backend/src/main/java/org/donggle/backend/ui/common/ErrorResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.donggle.backend.ui.common; - -public record ErrorResponse(String message) { -} diff --git a/backend/src/main/java/org/donggle/backend/ui/common/ErrorWrapper.java b/backend/src/main/java/org/donggle/backend/ui/common/ErrorWrapper.java new file mode 100644 index 000000000..442e51730 --- /dev/null +++ b/backend/src/main/java/org/donggle/backend/ui/common/ErrorWrapper.java @@ -0,0 +1,4 @@ +package org.donggle.backend.ui.common; + +public record ErrorWrapper(ErrorContent error) { +} diff --git a/backend/src/main/java/org/donggle/backend/ui/common/GlobalExceptionHandler.java b/backend/src/main/java/org/donggle/backend/ui/common/GlobalExceptionHandler.java index bea6582bd..39884b18d 100644 --- a/backend/src/main/java/org/donggle/backend/ui/common/GlobalExceptionHandler.java +++ b/backend/src/main/java/org/donggle/backend/ui/common/GlobalExceptionHandler.java @@ -25,33 +25,50 @@ @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException(final AuthenticationException e) { + public ResponseEntity handleAuthenticationException(final AuthenticationException e) { log.warn("Exception from handleAuthenticationException = ", e); - final ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(e.getMessage(), e.getHint(), e.getErrorCode()) + ); + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(errorWrapper); } - + @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(final NotFoundException e) { + public ResponseEntity handleNotFoundException(final NotFoundException e) { log.warn("Exception from handleNotFoundException = ", e); - final ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(e.getMessage(), e.getHint(), e.getErrorCode()) + ); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(errorWrapper); } - + @ExceptionHandler(BusinessException.class) - public ResponseEntity handleBusinessException(final BusinessException e) { + public ResponseEntity handleBusinessException(final BusinessException e) { log.warn("Exception from handleBusinessException = ", e); - final ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(e.getMessage(), e.getHint(), e.getErrorCode()) + ); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(errorWrapper); } - + @ExceptionHandler(IOException.class) - public ResponseEntity handleIOException(final IOException e) { + public ResponseEntity handleIOException(final IOException e) { log.warn("Exception from handleIOException = ", e); - final ErrorResponse errorResponse = new ErrorResponse("잘못된 파일 입력입니다."); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); + final String hint = "파일을 읽는 데에 문제가 발생했습니다."; + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.BAD_REQUEST.value()) + ); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(errorWrapper); } - + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { log.warn("Exception from handleMethodArgumentNotValidException = ", e); @@ -65,73 +82,89 @@ public ResponseEntity> handleMethodArgumentNotValidException .status(HttpStatus.BAD_REQUEST) .body(errors); } - + @ExceptionHandler(MissingServletRequestParameterException.class) - public ResponseEntity handleMissingServletRequestParameterException(final MissingServletRequestParameterException e) { + public ResponseEntity handleMissingServletRequestParameterException(final MissingServletRequestParameterException e) { log.warn("Exception from handleMissingServletRequestParameterException = ", e); - final ErrorResponse errorResponse = new ErrorResponse( - "잘못된 요청입니다. 누락된 쿼리 파라미터 타입: " + e.getParameterName() + - "예상되는 쿼리 파라미터 타입: " + e.getParameterType() + final String hint = "잘못된 요청입니다. 누락된 쿼리 파라미터 타입: " + e.getParameterName() + + "예상되는 쿼리 파라미터 타입: " + e.getParameterType(); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.BAD_REQUEST.value()) ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(errorResponse); + .body(errorWrapper); } - + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e) { + public ResponseEntity handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e) { log.warn("Exception from handleHttpRequestMethodNotSupportedException = ", e); - final ErrorResponse errorResponse = new ErrorResponse("잘못된 요청입니다. HTTP 메서드를 다시 확인해주세요. 입력한 HTTP 메서드: " + e.getMethod()); + final String hint = "잘못된 요청입니다. HTTP 메서드를 다시 확인해주세요. 입력한 HTTP 메서드: " + e.getMethod(); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.METHOD_NOT_ALLOWED.value()) + ); return ResponseEntity .status(HttpStatus.METHOD_NOT_ALLOWED) - .body(errorResponse); + .body(errorWrapper); } - + @ExceptionHandler(MissingPathVariableException.class) - public ResponseEntity handleMissingPathVariableException(final MissingPathVariableException e) { + public ResponseEntity handleMissingPathVariableException(final MissingPathVariableException e) { log.warn("Exception from handleMissingPathVariableException = ", e); - final ErrorResponse errorResponse = new ErrorResponse("잘못된 요청입니다. 누락된 경로 변수: " + e.getVariableName()); + final String hint = "잘못된 요청입니다. 누락된 경로 변수: " + e.getVariableName(); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.BAD_REQUEST.value()) + ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(errorResponse); + .body(errorWrapper); } - + @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException e) { + public ResponseEntity handleMethodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException e) { log.warn("Exception from handleMethodArgumentTypeMismatchException = ", e); - final ErrorResponse errorResponse = new ErrorResponse( - "잘못된 요청입니다. 잘못된 변수: " + e.getName() + - "예상되는 변수 타입: " + e.getRequiredType() + final String hint = "잘못된 요청입니다. 잘못된 변수: " + e.getName() + "예상되는 변수 타입: " + e.getRequiredType(); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.BAD_REQUEST.value()) ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(errorResponse); + .body(errorWrapper); } - + @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity handleHttpMessageNotReadableException(final HttpMessageNotReadableException e) { + public ResponseEntity handleHttpMessageNotReadableException(final HttpMessageNotReadableException e) { log.warn("Exception from handleHttpMessageNotReadableException = ", e); - final ErrorResponse errorResponse = new ErrorResponse("잘못된 요청입니다. 요청 바디를 다시 확인해주세요."); + final String hint = "잘못된 요청입니다. 요청 바디를 다시 확인해주세요."; + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.BAD_REQUEST.value()) + ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(errorResponse); + .body(errorWrapper); } - + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) - public ResponseEntity handleHttpMediaTypeNotSupportedException(final HttpMediaTypeNotSupportedException e) { + public ResponseEntity handleHttpMediaTypeNotSupportedException(final HttpMediaTypeNotSupportedException e) { log.warn("Exception from handleHttpMediaTypeNotSupportedException = ", e); - final ErrorResponse errorResponse = new ErrorResponse( - "잘못된 요청입니다. 지원하지 않는 미디어 타입입니다. 지원되는 미디어 타입: " + e.getSupportedMediaTypes() + final String hint = "잘못된 요청입니다. 지원하지 않는 미디어 타입입니다. 지원되는 미디어 타입: " + e.getSupportedMediaTypes(); + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.BAD_REQUEST.value()) ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(errorResponse); + .body(errorWrapper); } - + @ExceptionHandler(Exception.class) - public ResponseEntity handleUnExpectedException(final Exception e) { + public ResponseEntity handleUnExpectedException(final Exception e) { log.error("Exception from handleUnExpectedException = ", e); - final ErrorResponse errorResponse = new ErrorResponse("서버에서 예상치 못한 문제가 발생하였습니다."); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + final String hint = "서버에서 예상치 못한 문제가 발생하였습니다."; + final ErrorWrapper errorWrapper = new ErrorWrapper( + ErrorContent.of(hint, HttpStatus.INTERNAL_SERVER_ERROR.value()) + ); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorWrapper); } }