Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

be/feat/#158 be 무드 조회 api 구현 #160

Merged
merged 20 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1bb748c
feat: Mood 조회, Mood 랜덤 조회 기능 구현
sejeongsong Nov 14, 2023
202cbd9
chore: 성공케이스라는 문구 추가
sejeongsong Nov 14, 2023
f1314cd
fix: 에러 코드가 에러 메세지로 출력되는 오류 수정
sejeongsong Nov 14, 2023
1843b4b
feat: 무드 등록, 무드 id로 조회 기능 구현
sejeongsong Nov 14, 2023
51caba8
chore: whitelist 추가
sejeongsong Nov 14, 2023
385b30c
chore: docs -> acceptance로 패키지 이동, 패키지명 인수테스트로 변경
sejeongsong Nov 14, 2023
93d4f2a
fix: 비밀번호 검증이 누락된 오류 해결
sejeongsong Nov 15, 2023
1ef53ef
chore: DB에 존재하지 않는 Member Fixture는 비회원으로 수정
sejeongsong Nov 15, 2023
90be198
refactor: 인증 실패시 응답 상태코드 더 상세하게 분류
sejeongsong Nov 15, 2023
a2df764
test: 로그인 기능 인수 테스트 추가
sejeongsong Nov 15, 2023
8fddd0e
refactor: JwtUtil에서 클레임 관련 메서드들을 ClaimUtil로 분리
sejeongsong Nov 15, 2023
6febf06
test: 회원 가입 기능 인수테스트 작성
sejeongsong Nov 15, 2023
e9b92e3
refactor: 엔티티 -> dto 변환을 서비스에서 Mapper로 이동
sejeongsong Nov 15, 2023
733f12a
refactor: mood -> name 으로 파라미터명 변경
sejeongsong Nov 15, 2023
63806c6
refactor: Exception 클래스의 상속 깊이가 5를 넘지 않도록 수정
sejeongsong Nov 15, 2023
a822e19
test: 회원 프로필 조회 인수 테스트 추가
sejeongsong Nov 15, 2023
5d7077a
feat: 회원가입 시 이메일 형식 검증 추가
sejeongsong Nov 15, 2023
f71673e
refactor: 기본생성자 protected로 변경
sejeongsong Nov 15, 2023
bea474b
fix: PK의 타입이 String이라 findById가 안되는 오류 수정
sejeongsong Nov 15, 2023
481e83e
refactor: dto로 조회하는 메서드명을 전부 fetch로 변경
sejeongsong Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 87 additions & 3 deletions be/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,22 @@ operation::updateFeed[snippets='http-request']
operation::deleteFeed[snippets='http-request']

[[auth]]
== 보안
== 인증/인가

=== 로그인

==== 성공

operation::login_success[snippets='http-request,http-response']

=== 로그아웃
==== 가입되지 않은 이메일일 때

operation::login_failedByUnregisteredEmail[snippets='http-response']

==== 비밀번호가 틀렸을 때

operation::login_failedByWrongPassword[snippets='http-response']

operation::logout_success[snippets='http-request,http-response']

[[comment]]
== 댓글
Expand Down Expand Up @@ -138,3 +145,80 @@ operation::fetchComments_failed_by_feed_id_not_exists[snippets='http-response']
==== 성공

operation::signupMember_success[snippets='http-request,http-response']

==== 입력값이 잘못됐을 때

operation::signupMember_failedByMultipleInvalidInput[snippets='http-response']

==== 이미 가입된 이메일일 때

operation::signupMember_failedByDuplicateEmail[snippets='http-response']

==== 이미 가입된 닉네임일 때

operation::signupMember_failedByDuplicateNickname[snippets='http-response']

==== 재입력한 패스워드가 다를 때

operation::signupMember_failedByReconfirmPasswordUnmatch[snippets='http-response']

=== 회원 프로필 조회

==== 성공

operation::fetchMemberProfile_success[snippets='http-request,http-response']

==== 존재하지 않는 회원 id일 때

operation::fetchMemberProfile_failedByIdNotFound[snippets='http-response']


[[mood]]
== 무드

=== 무드 조회

==== 성공

operation::fetchSliceMood_success[snippets='http-request,http-response']

==== 성공 - 페이징

operation::fetchSliceMood_whenPageAndSizeExists_success[snippets='http-request,http-response']

=== 무드 랜덤 조회

==== 성공

operation::fetchRandomMood_success[snippets='http-request,http-response']

==== 성공 - DB에 무드가 없을 때

operation::fetchRandomMood_whenMoodNotExists_success[snippets='http-response']

==== 요청의 쿼리 파라미터에 count가 없을 때

operation::fetchRandomMood_failedByCountNull[snippets='http-request,http-response']

=== 무드 추가

==== 성공

operation::registerMood_success[snippets='http-request,http-response']

==== 이미 존재하는 무드일 때

operation::registerMood_failedByDuplicateName[snippets='http-response']

==== 요청으로 받은 무드가 null일 때

operation::registerMood_failedByNullName[snippets='http-response']

=== 개별 무드 조회

==== 성공

operation::findMoodyById_success[snippets='http-request,http-response']



48 changes: 48 additions & 0 deletions be/src/main/java/com/foodymoody/be/auth/util/ClaimUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.foodymoody.be.auth.util;

import com.foodymoody.be.common.exception.ClaimNotFoundException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.IncorrectClaimException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.RequiredTypeException;
import io.jsonwebtoken.security.Keys;
import java.util.Map;
import java.util.Objects;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ClaimUtil {

private String secret;
private SecretKey secretKey;

public ClaimUtil(@Value("${jwt.token.secret}") String secret){
this.secret = secret;
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
}

public Map<String, Object> createClaim(String key, String value) {
return Map.of(key, value);
}

public <T> T getClaim(Claims claims, String key, Class<T> type) {
try {
T claim = claims.get(key, type);
if (Objects.isNull(claim)) {
throw new ClaimNotFoundException();
}
return claim;
} catch (RequiredTypeException | ClassCastException e) {
throw new IncorrectClaimException(null, claims, key);
}
}

public Claims extractClaims(String token) {
JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
return parser.parseClaimsJws(token).getBody();
}

}
44 changes: 10 additions & 34 deletions be/src/main/java/com/foodymoody/be/auth/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package com.foodymoody.be.auth.util;

import com.foodymoody.be.common.exception.ClaimNotFoundException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.IncorrectClaimException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.RequiredTypeException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
Expand All @@ -25,36 +20,37 @@ public class JwtUtil {
private String secret;
private String issuer;
private SecretKey secretKey;
private JwtParser parser;
private ClaimUtil claimUtil;

public JwtUtil(
@Value("${jwt.token.exp.access}") long accessTokenExp,
@Value("${jwt.token.exp.refresh}")long refreshTokenExp,
@Value("${jwt.token.secret}") String secret,
@Value("${jwt.token.issuer}") String issuer) {
@Value("${jwt.token.issuer}") String issuer,
ClaimUtil claimUtil) {
this.accessTokenExp = accessTokenExp;
this.refreshTokenExp = refreshTokenExp;
this.secret = secret;
this.issuer = issuer;
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
this.parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
this.claimUtil = claimUtil;
}

public String createAccessToken(Date now, String id, String email) {
Map<String, Object> idClaim = createClaim("id", id);
Map<String, Object> emailClaim = createClaim("email", email);
Map<String, Object> idClaim = claimUtil.createClaim("id", id);
Map<String, Object> emailClaim = claimUtil.createClaim("email", email);
return createToken(now, accessTokenExp, issuer, secretKey, idClaim, emailClaim);
}

public String createRefreshToken(Date now, String id) {
Map<String, Object> idClaim = createClaim("id", id);
Map<String, Object> idClaim = claimUtil.createClaim("id", id);
return createToken(now, refreshTokenExp, issuer, secretKey, idClaim);
}

public Map<String, String> parseAccessToken(String token) {
Claims claims = extractClaims(token);
String id = getClaim(claims, "id", String.class);
String email = getClaim(claims, "email", String.class);
Claims claims = claimUtil.extractClaims(token);
String id = claimUtil.getClaim(claims, "id", String.class);
String email = claimUtil.getClaim(claims, "email", String.class);
return Map.of("id", id, "email", email);
}

Expand All @@ -70,24 +66,4 @@ private String createToken(Date now, long exp, String issuer, SecretKey secretKe
.setExpiration(expiration)
.signWith(secretKey, SignatureAlgorithm.HS256).compact();
}

private Map<String, Object> createClaim(String key, String value) {
return Map.of(key, value);
}

private <T> T getClaim(Claims claims, String key, Class<T> type) {
try {
T claim = claims.get(key, type);
if (Objects.isNull(claim)) {
throw new ClaimNotFoundException();
}
return claim;
} catch (RequiredTypeException | ClassCastException e) {
throw new IncorrectClaimException(null, claims, key);
}
}

private Claims extractClaims(String token) {
return parser.parseClaimsJws(token).getBody();
}
}
2 changes: 1 addition & 1 deletion be/src/main/java/com/foodymoody/be/common/WrappedId.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class WrappedId implements Serializable {

protected String id;

public WrappedId() {
protected WrappedId() {
}

public WrappedId(String id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ protected BusinessException(ErrorMessage errorMessage) {
}

public String getCode() {
return errorMessage.getMessage();
return errorMessage.getCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.foodymoody.be.common.exception;

public class DuplicateMoodException extends BusinessException {

public DuplicateMoodException() {
super(ErrorMessage.DUPLICATE_MOOD);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ public enum ErrorMessage {
DUPLICATE_MEMBER_EMAIL("이미 가입된 이메일입니다", "m002"),
DUPLICATE_MEMBER_NICKNAME("이미 존재하는 닉네임입니다", "m003"),
INVALID_CONFIRM_PASSWORD("입력하신 패스워드와 일치하지 않습니다", "m004"),
MEMBER_INCORRECT_PASSWORD("사용자 정보와 패스워드가 일치하지 않습니다", "m005"),
// auth
INVALID_TOKEN("토큰이 유효하지 않습니다", "a001"),
IS_NOT_HTTP_REQUEST("http 요청이 아닙니다", "a002"),
// auth,
UNAUTHORIZED("권한이 없습니다", "a001"),
INVALID_TOKEN("토큰이 유효하지 않습니다", "a002"),
CLAIM_NOT_FOUND("토큰에 해당 클레임이 존재하지 않습니다", "a003"),
UNAUTHORIZED("권한이 없습니다", "a004"),
INVALID_ACCESS_TOKEN("유효하지 않은 액세스 토큰입니다", "a005");
INVALID_ACCESS_TOKEN("유효하지 않은 액세스 토큰입니다", "a004"),
MEMBER_INCORRECT_PASSWORD("사용자 정보와 패스워드가 일치하지 않습니다", "a005"),
// mood
DUPLICATE_MOOD("이미 존재하는 무드입니다", "o001");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static com.foodymoody.be.common.exception.ErrorMessage.INVALID_INPUT_VALUE;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;

import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -10,6 +12,7 @@
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestValueException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand Down Expand Up @@ -42,11 +45,32 @@ public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadabl

@ResponseStatus(value = BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(HttpMessageNotReadableException e) {
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
log.error("handleIllegalArgumentException", e);
return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode());
}

@ResponseStatus(value = BAD_REQUEST)
@ExceptionHandler(MissingRequestValueException.class)
public ErrorResponse handleIllegalArgumentException(MissingRequestValueException e) {
log.error("handleMissingRequestValueExceptionException", e);
return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode());
}

@ResponseStatus(value = UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
public ErrorResponse handleUnauthorizedException(UnauthorizedException e) {
log.error("handleUnauthorizedExceptionException", e);
return new ErrorResponse(e.getMessage(), e.getCode());
}

@ResponseStatus(value = NOT_FOUND)
@ExceptionHandler(ResourceNotFoundException.class)
public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException e) {
log.error("handleResourceNotFoundExceptionException", e);
return new ErrorResponse(e.getMessage(), e.getCode());
}

private static Map<String, String> getErrors(MethodArgumentNotValidException e) {
return e.getBindingResult()
.getAllErrors()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.foodymoody.be.common.exception;

public class IncorrectMemberPasswordException extends BusinessException {
public class IncorrectMemberPasswordException extends UnauthorizedException {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception -> RuntimeException -> BusinessException -> UnauthorizedException -> IncorrectMemberPasswordException
deep가 너무 깊어서 추천하지 않아요.~ 원인은 한번 확인 해봐요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부모 클래스가 많을수록 클래스가 복잡해지고, 불필요하게 결합도가 높아지는 것 같습니다.. BusinessException을 상속받지 않도록 수정하겠습니다!


public IncorrectMemberPasswordException() {
super(ErrorMessage.MEMBER_INCORRECT_PASSWORD);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.foodymoody.be.common.exception;

public class MemberNotFoundException extends BusinessException {
public class MemberNotFoundException extends ResourceNotFoundException {

public MemberNotFoundException() {
super(ErrorMessage.MEMBER_NOT_FOUND);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.foodymoody.be.common.exception;

public abstract class ResourceNotFoundException extends RuntimeException{

private final ErrorMessage errorMessage;

protected ResourceNotFoundException(ErrorMessage errorMessage) {
super(errorMessage.getMessage());
this.errorMessage = errorMessage;
}

public String getCode() {
return errorMessage.getCode();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.foodymoody.be.common.exception;

public class UnauthorizedException extends BusinessException{
public class UnauthorizedException extends RuntimeException{

// TODO 적절한 표준 예외로 리팩토링
public UnauthorizedException() {
super(ErrorMessage.UNAUTHORIZED);
private final ErrorMessage errorMessage;

protected UnauthorizedException(ErrorMessage errorMessage) {
super(errorMessage.getMessage());
this.errorMessage = errorMessage;
}

public String getCode() {
return errorMessage.getCode();
}
}
Loading