Skip to content

Commit

Permalink
[feat] 사용자 가입승인 기능 구현 (#26)
Browse files Browse the repository at this point in the history
* feat : 사용자 로그인 기능
- accessToken을 이용한 로그인 기능 먼저 구현
- UserDetail과 UserDetailService 생성
- UserRepository 작성
- 토큰 생성, 유효성 검증, 토큰에서 필요한 정보 가져오는 TokenProvider 클래스 생성
- 발급받은 토큰을 검증하고 다음 필터로 전달하는 TokenAuthenticationFilter 클래스 생성
- Spring Security 설정을 위한 SecurityConfig 작성
- UserController 작성

* refactor : 파일명과 변수명에서 게시물을 뜻하는 content를 post로 변경

* refactor : Long에서 String으로 자료형 변경

* feat : 로그인시 리프레시토큰 저장 및 리프레시토큰과 액세스토큰 재발급
- 기존의 로그인시 액세스토큰만 발급되던 코드에 리프레시토큰도 발급되어 저장되는 로직 UserService에 추가
- 로그인해도 토큰이 바뀌지 않는 문제 TokenProvider에서 수정
-- claim에 발급 시간과 만료 시간 추가
- TokenService에서 리프레시 토큰을 검증해 액세스토큰과 리프레시토큰 재발급하는 로직 추가

* feat : users SecurityConfig 비밀번호 암호화 기능 추가

* refactor : Post 엔티티 수정

- Content -> Post로 클래스 네임 변경
- PK : Long -> String 으로 타입 변경
- ContentController -> PostController 클래스 네임 변경

* refactor : ContentRepository 삭제

* build : QueryDsl dependency 추가

- Q클래스 폴더 ignore 처리

* chore : QueryDslConfig 클래스 생성

* feat : String으로 받아온 파라미터를 LocalDateTime으로 바꿔주는 Converter 설정

* feat : 게시물 통계 기능 구현

* refactor : ErrorResponse 클래스를 record 클래스로 변경

* feat : 400에러를 처리하기 위한 Exception 생성

- StatParam : 사용하지 않는 Exception 제거
- Handler : 잘못된 import 변경

* feat : api 테스트를 위한 security 임의 설정

* refactor : 리뷰사항 반영

post 테이블 명 변경 post -> posts

* refactor : 리뷰사항 반영

post 테이블의 id 타입을 String으로 변경하면서 GeneratedValue 어노테이션 제거

* feat : api 테스트를 위한 security 임시 설정

* feat : Code 클래스 @builder 추가

* feat : 사용자 회원가입 기능 구현

* refactor : 가독성 위한 코드 수정 및 에러 핸들링 추가
- TokenProvider의 makeToken 메서드 가독성을 위한 수정
- 회원이 존재하지 않는 경우의 에러 핸들링 추가

* refactor : 토큰 만료시간 수정

* refactor : 필드 공백 제거

* refactor : 코드 재발급 메서드 명칭 수정
- TokenController과 TokenService의 재발급 메서드 명칭 getToken으로 수정
- 직관성을 위해 URL도 수정

* refactor : 로그인 메서드 명칭 ㅅ정

* refactor : TokenRequestDto record 클래스로 수정

* fix : String 타입은 IDENTITY 적용할 수 없어서 제거함

* refactor : JPA 변경 감지 기능에 따른 save() 메서드 삭제

* refactor : Service에 트랜잭션 설정 추가

* feat : 게시글 좋아요 기능 구현

* refactor :TokenProvider에 상수 변경

* refactor : TokenRepository 쿼리문 삭제

* refactor :  UserCreateDto → UserInfoDto로 이름 변경

* refactor : UserCreateDto → UserInfoDto로 이름 변경

* feat : 회원등급 속성 추가

* feat : UserInfoDto 회원등급 속성 추가

* feat : 사용자 가입승인 기능 구현

* style : 패키지명 변경

* refactor : yml 파일 추가

* fix : 동시성 문제로 인한 에러 해결
- @query 대신 addLikeCount()를 사용하여 JPA 변경 감지를 통해 업데이트 반영하여 해결

* refactor : 게시물 좋아요 기능 로직 파일 변경 PostLike~ → Post~

* feat : PostIdResponse를 사용하여 응답 형식 구조 개선

* refactor : dev yml파일 수정

* refactor :  필드 공백 제거

* refactor : addLikeCount()의 파라미터 제거로 간결하게 개선

* no message

* refactor : SignUpRequset, SignUpResponse, UserCreateDto record 클래스로 변경

* refactor : access level 설정

* refactor : 통상적으로 자주 사용되는 비밀번호 검사 코드 수정

* feat : 인증코드 발급 방식을 uuid로 변경

* refactor : record 클래스 변경으로 회원가입 api 코드 수정

* refactor : 필드 공백 제거

* refactor : 필드 공백 제거

* refactor : PostIdResponse를 에러 반환에 적용하여 응답 형식 개선

* refactor : 통상적으로 사용되는 비밀번호 검증 삭제

* refactor : 로그인시 비밀번호 암호화 적용 추가

* [feat] post 상세보기 api (#27)

* feat: post 상세보기 api

* refactor: 테스트용 security 설정

* refactor: review 반영

- 공백 제거
- dto record로 변경
- DetailResponse 반환 코드 PostService -> PostController 수정 작성
- .gitignore 수정
- PostNotFoundException -> NotFoundException

* refactor: repository test code 삭제

* refactor: Entity, DTO Colum post -> content 변경

* fix: conflict 해결

* feat : 에러를 처리하기 위한 Exception 생성

* refactor : VerifyRequest, VerifyResponse, UserInfoDto radio 클래스로 변경

* refactor : Exception 코드 수정

* feat : 사용자 인증코드 조회 쿼리 추가

* feat : 인증코드 재발급 기능 추가

* Changes

* feat : 회원등급 속성 추가

* Changes

* Changes

* refactor : VerifyRequest, VerifyResponse, UserInfoDto radio 클래스로 변경

* Changes

* feat : 사용자 인증코드 조회 쿼리 추가

* Changes

---------

Co-authored-by: LeeJiWon <[email protected]>
Co-authored-by: Jinhui <[email protected]>
Co-authored-by: pie <[email protected]>
Co-authored-by: pie <[email protected]>
Co-authored-by: ssunnykku <[email protected]>
Co-authored-by: jiwon <[email protected]>
  • Loading branch information
7 people authored Aug 26, 2024
1 parent 3cf7a40 commit 1322c34
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 10 deletions.
7 changes: 6 additions & 1 deletion src/main/java/wanted/media/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ public enum ErrorCode {
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."),
// 클라이언트의 입력 값에 대한 일반적인 오류 (@PathVariable, @RequestParam가 잘못되었을 때)
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "클라이언트의 입력 값을 확인해주세요."),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.");
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "인증코드가 일치하지 않습니다."),
VERIFICATION_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "만료된 인증코드입니다.");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package wanted.media.exception;

public class InvalidPasswordException extends BaseException {
public InvalidPasswordException() {

super(ErrorCode.INVALID_PASSWORD);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wanted.media.exception;

public class UserNotFoundException extends BaseException {
public UserNotFoundException() {
super(ErrorCode.USER_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wanted.media.exception;

public class VerificationCodeExpiredException extends BaseException {
public VerificationCodeExpiredException() {
super(ErrorCode.VERIFICATION_CODE_EXPIRED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wanted.media.exception;

public class VerificationCodeMismatchException extends BaseException {
public VerificationCodeMismatchException() {
super(ErrorCode.VERIFICATION_CODE_MISMATCH);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import wanted.media.exception.CustomException;
import wanted.media.exception.BaseException;
import wanted.media.exception.ErrorCode;
import wanted.media.exception.CustomException;
import wanted.media.exception.ErrorResponse;
import wanted.media.exception.NotFoundException;

Expand All @@ -18,7 +19,6 @@ public ResponseEntity<ErrorResponse> handleBadRequestException(BadRequestExcepti
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, e.getMessage()));
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handlePostNotFound(NotFoundException e) {
ErrorCode errorCode = e.getErrorCode();
Expand All @@ -28,11 +28,16 @@ public ResponseEntity<ErrorResponse> handlePostNotFound(NotFoundException e) {
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}

@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(final CustomException e) {
return ResponseEntity
.status(e.getErrorCode().getStatus().value())
.body(new ErrorResponse(e.getErrorCode().getStatus().value(), e.getCustomMessage()));
}
@ExceptionHandler(BaseException.class)
public ResponseEntity<ErrorResponse> handleBaseException(BaseException e) {
ErrorCode errorCode = e.getErrorCode();
return ResponseEntity.status(errorCode.getStatus())
.body(new ErrorResponse(errorCode.getStatus().value(), errorCode.getMessage()));
}
}
22 changes: 21 additions & 1 deletion src/main/java/wanted/media/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import org.springframework.web.bind.annotation.RestController;
import wanted.media.user.dto.SignUpRequest;
import wanted.media.user.dto.SignUpResponse;
import wanted.media.user.dto.VerifyRequest;
import wanted.media.user.dto.VerifyResponse;
import wanted.media.user.dto.UserLoginRequestDto;
import wanted.media.user.dto.UserLoginResponseDto;
import wanted.media.user.dto.*;
import wanted.media.user.service.UserService;

@RestController
Expand All @@ -26,10 +29,27 @@ public ResponseEntity<UserLoginResponseDto> loginUser(@RequestBody UserLoginRequ
return ResponseEntity.ok().body(responseDto);
}

//회원가입
// 회원가입 API
@PostMapping("/sign-up")
public ResponseEntity<SignUpResponse> signUp(@Validated @RequestBody SignUpRequest request) {
SignUpResponse response = userService.signUp(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

/*
* 가입승인 API
* 회원등급 (normal -> premium)
* */
@PostMapping("/approve")
public ResponseEntity<VerifyResponse> approveSignUp(@RequestBody VerifyRequest request) {
VerifyResponse response = userService.approveSignUp(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

// 인증코드 재발급 요청 API
@PostMapping("/reissue-code")
public ResponseEntity<ReissueCodeResponse> reissueCode(@RequestBody ReissueCodeRequest request) {
ReissueCodeResponse response = userService.reissueCode(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
2 changes: 1 addition & 1 deletion src/main/java/wanted/media/user/domain/Code.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class Code {
@Column(nullable = false)
private Long codeId;

@OneToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/wanted/media/user/dto/ReissueCodeRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package wanted.media.user.dto;

public record ReissueCodeRequest(String account, String password) {
}
4 changes: 4 additions & 0 deletions src/main/java/wanted/media/user/dto/ReissueCodeResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package wanted.media.user.dto;

public record ReissueCodeResponse(String message, String newAuthCode) {
}
6 changes: 6 additions & 0 deletions src/main/java/wanted/media/user/dto/UserInfoDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package wanted.media.user.dto;

import wanted.media.user.domain.Grade;

public record UserInfoDto(String account, String email, Grade grade) {
}
11 changes: 11 additions & 0 deletions src/main/java/wanted/media/user/dto/VerifyRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package wanted.media.user.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record VerifyRequest(
@NotBlank @Size(max = 50) String account,
@NotBlank @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") String password,
@NotBlank @Size(max = 10) String inputCode
) {
}
4 changes: 4 additions & 0 deletions src/main/java/wanted/media/user/dto/VerifyResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package wanted.media.user.dto;

public record VerifyResponse(String message, UserInfoDto dto) {
}
17 changes: 15 additions & 2 deletions src/main/java/wanted/media/user/repository/CodeRepository.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package wanted.media.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import wanted.media.user.domain.Code;
import wanted.media.user.domain.User;

import java.util.List;
import java.util.Optional;

public interface CodeRepository extends JpaRepository<Code, Long> {
// 사용자별 인증코드 중복확인
boolean existsByUserAndAuthCode(User user, String newAuthCode);
// 특정 사용자 인증코드로 조회
Optional<Code> findByUserAndAuthCode(User user, String authCode);

//사용자가 발급받은 인증코드 삭제
void deleteByUser(User user);

// 사용자에 대해 모든 인증 코드 조회
@Query("SELECT c FROM Code c WHERE c.user = :user ORDER BY c.createdTime DESC")
List<Code> findAllByUserOrderByCreatedTimeDesc(@Param("user") User user);

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package wanted.media.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import wanted.media.user.domain.Grade;
import wanted.media.user.domain.User;

import java.util.Optional;
Expand All @@ -12,4 +16,9 @@ public interface UserRepository extends JpaRepository<User, UUID> {

// 사용자 이메일로 회원 조회
Optional<User> findByEmail(String email);

// 가입인증 회원 등급 변경
@Modifying
@Query("UPDATE User u SET u.grade = :grade WHERE u.account = :account")
void updateUserGrade(@Param("account") String account, @Param("grade") Grade grade);
}
62 changes: 60 additions & 2 deletions src/main/java/wanted/media/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package wanted.media.user.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import wanted.media.user.config.TokenProvider;
import wanted.media.exception.InvalidPasswordException;
import wanted.media.exception.UserNotFoundException;
import wanted.media.exception.VerificationCodeExpiredException;
import wanted.media.exception.VerificationCodeMismatchException;
import wanted.media.user.domain.Code;
import wanted.media.user.domain.Grade;
import wanted.media.user.domain.Token;
Expand All @@ -16,6 +20,7 @@

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.List;

@Service
@RequiredArgsConstructor
Expand All @@ -29,7 +34,7 @@ public class UserService {
private final UserValidator userValidator;
private final GenerateCode generateCode;

@Transactional

public UserLoginResponseDto login(UserLoginRequestDto requestDto) {
User user = userRepository.findByAccount(requestDto.getAccount())
.orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요."));
Expand Down Expand Up @@ -77,4 +82,57 @@ public SignUpResponse signUp(SignUpRequest request) {
// 9. SignUpResponse 생성 및 반환
return new SignUpResponse("회원가입이 완료되었습니다.", userCreateDto, verificationCode);
}

//가입승인
public VerifyResponse approveSignUp(VerifyRequest verifyRequest) {
// 1. account로 사용자 조회
User user = userRepository.findByAccount(verifyRequest.account())
.orElseThrow(UserNotFoundException::new);
// 2. 비밀번호 검증
if (!passwordEncoder.matches(verifyRequest.password(), user.getPassword())) {
throw new InvalidPasswordException();
}
// 3. 사용자 인증코드 검증
Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.inputCode())
.orElseThrow(VerificationCodeMismatchException::new);
// 4. 해당 사용자의 모든 인증코드 조회 (최신순 정렬)
List<Code> userCodes = codeRepository.findAllByUserOrderByCreatedTimeDesc(user);
// 5. 해당 사용자에게 발급된 모든 인증코드와 입력된 인증코드 일치 조회
if (!userCodes.isEmpty() && !userCodes.get(0).equals(code)) {
throw new VerificationCodeMismatchException();
}
// 6. 인증코드 유효성 검증 (유효시간 15분)
if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) {
throw new VerificationCodeExpiredException();
}
// 7. 인증 완료 -> 회원 등급 변경 (normal -> premium)
userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER);
// 8. 인증 완료 회원 인증코드 삭제
codeRepository.deleteByUser(user);
return new VerifyResponse("인증이 성공적으로 완료되었습니다!",
new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade()));
}

// 인증코드 재발급
public ReissueCodeResponse reissueCode(ReissueCodeRequest reissueCodeRequest) {
// 1. account로 사용자 조회
User user = userRepository.findByAccount(reissueCodeRequest.account())
.orElseThrow(UserNotFoundException::new);
// 2. 비밀번호 검증
if (!passwordEncoder.matches(reissueCodeRequest.password(), user.getPassword())) {
throw new InvalidPasswordException();
}
// 3. 새로운 인증코드 발급
String newAuthCode = generateCode.codeGenerate();
// 4. 코드 객체 생성
Code newCode = Code.builder()
.user(user)
.authCode(newAuthCode)
.createdTime(LocalDateTime.now())
.build();
// 5. 코드 db 저장
codeRepository.save(newCode);

return new ReissueCodeResponse("인증코드가 성공적으로 재발급되었습니다.", newAuthCode);
}
}

0 comments on commit 1322c34

Please sign in to comment.