팀 프로젝트에서 로그인/회원가입 부분을 했고, 프로필 부분을 구현을 하려고한다.
로그인 한 사람이 자신의 프로필 , 마이페이지를 볼 수 있고 그 프로필을 들어가면 자기가 회원가입 했을때 썼던
정보를 볼 수 있고, 수정도 가능하며, 비밀번호 수정도 할 수 있다. 비밀번호 수정에는 예외처리가 들어가있다.
- 사용자 인증 기능
- 조건
- username: 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)
- password: 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자
- 회원가입 기능
- 성공
- DB에 중복된 username이 없다면 회원을 저장한다.
- 클라이언트에 성공 메시지와 상태코드를 반환한다.
- 응답은 content-type application/json 형식입니다.
- 회원 권한 부여
- ADMIN
- 모든 게시글, 댓글 수정과 삭제 가능
- USER
- 본인이 작성한 게시글, 댓글에 한하여 수정과 삭제 가능
- ADMIN
- ⚠️ 필수 예외처리
- DB에 중복된 username이 이미 존재하는 경우
- username,password 조건에 맞지 않는 경우
- 참고자료
- 성공
- 로그인 기능
- 성공
- DB에서 username을 사용하여 회원가입 된 사용자인지 확인한다.
- 회원가입 된 사용자라면 password를 비교하여 로그인한다.
- header에 토큰을 추가하고 성공 상태코드와 메세지를 반환합니다.
- ⚠️ 필수 예외처리
- 유효하지 않은 사용자 정보로 로그인을 시도한 경우
- ex. 회원가입을 하지 않거나 회원 탈퇴한 경우
- username과 password가 일치하지 않는 사용자 정보로 로그인을 시도한 경우
- 성공
- 사용자는 자신의 계정으로 서비스에 로그인할 수 있습니다.
- 로그아웃 기능
- 조건
- 로그아웃 시, 발행한 토큰은 초기화 합니다.
- 로그아웃 후 초기화 된 Refresh Token은 재사용할 수 없고 재로그인해야 합니다.
- 조건
- 사용자는 로그인 되어 있는 본인의 계정을 로그아웃 할 수 있습니다.
- 조건
이거는 사용자 인증 기능 , 이미 했고 프로필 부분의 요구 사항이다.
- 프로필 관리
- 비밀번호 수정 조건
- 비밀번호 수정 시, 본인 확인을 위해 현재 비밀번호를 입력하여 올바른 경우에만 수정할 수 있습니다.
- 비밀번호는 현재 비밀번호 및 최근 사용한 세 개의 비밀번호와 다르게 설정해야 합니다.
- 예시) 현재 비밀번호가 'currentPass'이고 이전 비밀번호가 'password1', 'password2', 'password3'였다면 새로운 비밀번호는 이 네 개와 다른 새로운 비밀번호여야 합니다.
- 프로필 수정 기능
- 이름, 한 줄 소개와 같은 기본적인 정보를 볼 수 있어야 하며 수정할 수 있어야 합니다.
- 비밀번호 수정 조건
엔티티다.
/**
* 사용자의 이전 비밀번호를 저장하는 엔티티
*/
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "password_list")
public class PasswordList {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String password;
@CreatedDate
@Column(nullable = false)
private LocalDateTime createdAt;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
}
/**
* 사용자 엔티티 클래스 데이터베이스의 사용자 정보를 나타내는 클래스
*/
@Entity
@Getter
@NoArgsConstructor
@Table(name = "users")
public class User extends TimeStamp {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 20)
private String userId;
@Column(nullable = false)
private String password;
@Column(nullable = false, length = 50)
private String userName;
@Column(nullable = false, unique = true, length = 50)
private String email;
@Column(length = 100)
private String intro;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserAuth userAuth;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserStatus userStatus;
@Column(unique = true)
private String refreshToken;
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<Menu> menu = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<Review> comments = new ArrayList<>();
public User(SignupRequestDto requestDto, UserStatus userStatus, UserAuth userAuth) {
this.userId = requestDto.getUserId();
this.password = requestDto.getPassword();
this.userName = requestDto.getUserName();
this.email = requestDto.getEmail();
this.intro = requestDto.getIntro();
this.userStatus = userStatus;
this.userAuth = userAuth;
}
public void updateUserStatus(UserStatus userStatus) {
this.userStatus = userStatus;
}
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void updatePassword(String newPassword) {
this.password = newPassword;
}
public void encryptionPassword(String password) {
this.password = password;
}
public void updateUserName(String userName) {
this.userName = userName;
}
public void updateIntro(String intro) {
this.intro = intro;
}
public void updateProfile(AdminUserProfileRequestDto requestDto) {
this.userName = requestDto.getUserName();
this.intro = requestDto.getIntro();
}
public void updateAuth(UserAuth userAuth) {
this.userAuth = userAuth;
}
}
레포지토리다.
/**
* PasswordList 엔티티를 위한 리포지토리 인터페이스
*/
public interface PasswordListRepository extends JpaRepository<PasswordList, Long> {
List<PasswordList> findByUserOrderByCreatedAtDesc(User user);
}
서비스다.
/**
* ProfileService는 사용자 프로필 및 비밀번호 변경 관련 비즈니스 로직을 처리
*/
@Service
public class ProfileService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final PasswordListRepository passwordListRepository;
public ProfileService(UserRepository userRepository, PasswordEncoder passwordEncoder, PasswordListRepository passwordListRepository) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.passwordListRepository = passwordListRepository;
}
@Transactional(readOnly = true)
public ProfileResponseDto getProfile(User user) {
return new ProfileResponseDto(user);
}
@Transactional
public ProfileResponseDto updateProfile(User user, ProfileRequestDto requestDto) {
user.updateUserName(requestDto.getUserName());
user.updateIntro(requestDto.getIntro());
userRepository.save(user);
return new ProfileResponseDto(user);
}
@Transactional
public void updatePassword(User user, PasswordUpdateRequestDto requestDto) {
if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new InvalidEnteredException("현재 비밀번호와 일치하지 않습니다.");
}
List<PasswordList> pastPasswords = passwordListRepository.findByUserOrderByCreatedAtDesc(user);
String newPasswordEncoded = passwordEncoder.encode(requestDto.getNewPassword());
if (pastPasswords.stream().anyMatch(pastPassword -> passwordEncoder.matches(requestDto.getNewPassword(), pastPassword.getPassword()))) {
throw new InvalidEnteredException("새로운 비밀번호는 현재 비밀번호 및 최근 사용한 비밀번호와 달라야 합니다.");
}
if (pastPasswords.size() >= 3) {
PasswordList oldestPassword = pastPasswords.remove(0);
passwordListRepository.delete(oldestPassword);
}
PasswordList newPasswordList = PasswordList.builder()
.password(newPasswordEncoded)
.user(user)
.build();
passwordListRepository.save(newPasswordList);
user.updatePassword(newPasswordEncoded);
userRepository.save(user);
}
}
Controller다.
/**
* ProfileController는 사용자 프로필 관련 API 요청을 처리
* - GET /users/profile: 프로필 조회
* - PUT /users/profile: 프로필 수정
* - PUT /users/profile/password: 비밀번호 수정
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/users/profile")
public class ProfileController {
private final ProfileService profileService;
@GetMapping
public ResponseEntity<StatusCommonResponse<ProfileResponseDto>> getProfile(@AuthenticationPrincipal UserDetailsImpl userDetails) {
ProfileResponseDto responseDto = profileService.getProfile(userDetails.getUser());
return ResponseEntity.ok(new StatusCommonResponse<>(200, "마이페이지 조회 성공", responseDto));
}
@PutMapping
public ResponseEntity<StatusCommonResponse<ProfileResponseDto>> updateProfile(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody ProfileRequestDto requestDto) {
ProfileResponseDto responseDto = profileService.updateProfile(userDetails.getUser(), requestDto);
return ResponseEntity.ok(new StatusCommonResponse<>(200, "마이페이지 수정 성공", responseDto));
}
@PutMapping("/password")
public ResponseEntity<StatusCommonResponse<String>> updatePassword(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody PasswordUpdateRequestDto requestDto) {
profileService.updatePassword(userDetails.getUser(), requestDto);
return ResponseEntity.ok(new StatusCommonResponse<>(200, "비밀번호 수정 성공"));
}
}
dto
/**
* 비밀번호 변경 요청 DTO
*/
@Getter
@NoArgsConstructor
public class PasswordUpdateRequestDto {
@NotBlank(message = "기존 패스워드를 입력해주세요.")
private String password;
@NotBlank(message = "새로운 패스워드를 입력해주세요.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]).{8,15}$",
message = "비밀번호는 대소문자 영문, 숫자, 특수문자를 최소 1글자씩 포함하며 최소 8자에서 15자 사이여야 합니다.")
private String newPassword;
}
/**
* 프로필 수정 요청 DTO
*/
@Getter
@NoArgsConstructor
public class ProfileRequestDto {
@NotBlank(message = "사용자 이름을 입력해주세요")
private String userName;
@NotBlank(message = "한 줄 소개를 입력해주세요")
private String intro;
}
Excetpion도 추가를 했다.
/**
* 입력된 값이 유효하지 않은 경우
* @param ex : InvalidEnteredException[custom]
* @return : error message, HttpStatus.BAD_REQUEST => 400
*/
@ExceptionHandler(InvalidEnteredException.class)
public ResponseEntity<String> invalidEnteredException(InvalidEnteredException ex) {
log.error("{}", ex.getMessage());
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
public class InvalidEnteredException extends RuntimeException {
public InvalidEnteredException(String message) {
super(message);
}
}
'TIL' 카테고리의 다른 글
TIL - 2024/06/26 (0) | 2024.06.26 |
---|---|
TIL - 2024/06/25 (0) | 2024.06.25 |
TIL - 2024/06/21 (0) | 2024.06.21 |
TIL - 2024/06/20 (0) | 2024.06.20 |
TIL - 2024/06/19 (0) | 2024.06.19 |