Skip to content

Commit fb1c75e

Browse files
Merge pull request #59 from prgrms-web-devcourse-final-project/feat/#58
[User] 마이페이지 조회, 수정, 탈퇴 구현
2 parents 8b5cb0d + 84f01fb commit fb1c75e

13 files changed

Lines changed: 277 additions & 12 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@
77
import com.back.web7_9_codecrete_be.domain.users.entity.User;
88
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
99
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
10+
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
1011
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.security.crypto.password.PasswordEncoder;
1314
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
1416

1517
import java.security.SecureRandom;
1618
import java.time.LocalDate;
1719

1820
@Service
1921
@RequiredArgsConstructor
22+
@Transactional
2023
public class AuthService {
2124
private final UserRepository userRepository;
2225
private final PasswordEncoder passwordEncoder;
@@ -38,7 +41,7 @@ public void signUp(SignupRequest req) {
3841

3942
// 닉네임 중복 체크
4043
if (userRepository.existsByNickname(req.getNickname())) {
41-
throw new BusinessException(AuthErrorCode.NICKNAME_DUPLICATED);
44+
throw new BusinessException(UserErrorCode.NICKNAME_DUPLICATED);
4245
}
4346

4447
User user = User.builder()
@@ -59,6 +62,10 @@ public LoginResponse login(LoginRequest req) {
5962
User user = userRepository.findByEmail(req.getEmail())
6063
.orElseThrow(() -> new BusinessException(AuthErrorCode.USER_NOT_FOUND));
6164

65+
if (user.getIsDeleted()) {
66+
throw new BusinessException(UserErrorCode.USER_DELETED);
67+
}
68+
6269
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
6370
throw new BusinessException(AuthErrorCode.INVALID_PASSWORD);
6471
}
@@ -87,6 +94,10 @@ public void resetPassword(String email) {
8794
User user = userRepository.findByEmail(email)
8895
.orElseThrow(() -> new BusinessException(AuthErrorCode.USER_NOT_FOUND));
8996

97+
if (user.getIsDeleted()) {
98+
throw new BusinessException(UserErrorCode.USER_DELETED);
99+
}
100+
90101
String tempPassword = generateTempPassword();
91102

92103
// 비밀번호 변경

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/TokenService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.back.web7_9_codecrete_be.domain.users.entity.User;
66
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
77
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
8+
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
89
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
910
import com.back.web7_9_codecrete_be.global.rq.Rq;
1011
import com.back.web7_9_codecrete_be.global.security.JwtProperties;
@@ -63,8 +64,11 @@ public String reissueAccessToken() {
6364
User user = userRepository.findByEmail(email)
6465
.orElseThrow(() -> new BusinessException(AuthErrorCode.USER_NOT_FOUND));
6566

66-
String savedRefresh =
67-
refreshTokenRedisRepository.findByUserId(user.getId());
67+
if (user.getIsDeleted()) {
68+
throw new BusinessException(UserErrorCode.USER_DELETED);
69+
}
70+
71+
String savedRefresh = refreshTokenRedisRepository.findByUserId(user.getId());
6872

6973
if (savedRefresh == null || !savedRefresh.equals(refresh)) {
7074
throw new BusinessException(AuthErrorCode.INVALID_TOKEN);
Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,76 @@
11
package com.back.web7_9_codecrete_be.domain.users.controller;
22

3+
import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;
4+
import com.back.web7_9_codecrete_be.domain.users.dto.request.UserUpdateNicknameRequest;
5+
import com.back.web7_9_codecrete_be.domain.users.dto.request.UserUpdatePasswordRequest;
6+
import com.back.web7_9_codecrete_be.domain.users.dto.response.UserResponse;
7+
import com.back.web7_9_codecrete_be.domain.users.entity.User;
38
import com.back.web7_9_codecrete_be.domain.users.service.UserService;
9+
import com.back.web7_9_codecrete_be.global.rq.Rq;
10+
import com.back.web7_9_codecrete_be.global.rsData.RsData;
11+
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.tags.Tag;
13+
import jakarta.validation.Valid;
414
import lombok.RequiredArgsConstructor;
5-
import org.springframework.web.bind.annotation.RequestMapping;
6-
import org.springframework.web.bind.annotation.RestController;
15+
import org.springframework.web.bind.annotation.*;
16+
import org.springframework.web.multipart.MultipartFile;
717

818
@RestController
919
@RequestMapping("/api/v1/users")
1020
@RequiredArgsConstructor
21+
@Tag(name = "User", description = "사용자 정보 API")
1122
public class UserController {
1223
private final UserService userService;
24+
private final TokenService tokenService;
25+
private final Rq rq;
26+
27+
@Operation(summary = "내 정보 조회", description = "현재 로그인된 사용자의 정보를 조회합니다.")
28+
@GetMapping("/me")
29+
public RsData<?> getMyInfo() {
30+
User user = rq.getUser();
31+
UserResponse response = userService.getMyInfo(user);
32+
return RsData.success("사용자 정보 조회 성공", response);
33+
}
34+
35+
@Operation(summary = "내 닉네임 수정", description = "닉네임을 수정합니다.")
36+
@PatchMapping("/nickname")
37+
public RsData<?> updateNickname(
38+
@Valid @RequestBody UserUpdateNicknameRequest req
39+
) {
40+
User user = rq.getUser();
41+
UserResponse response = userService.updateNickname(user, req);
42+
return RsData.success("사용자 닉네임 변경 완료", response);
43+
}
44+
45+
@Operation(summary = "내 프로필 이미지 수정", description = "프로필 이미지를 수정합니다.")
46+
@PatchMapping("/profile-image")
47+
public RsData<?> updateProfileImage(
48+
@RequestPart MultipartFile file
49+
) {
50+
User user = rq.getUser();
51+
String imageUrl = userService.updateProfileImage(user, file);
52+
return RsData.success("프로필 이미지가 변경되었습니다.", imageUrl);
53+
}
54+
55+
@Operation(summary = "비밀번호 변경", description = "현재 비밀번호를 확인한 후 새로운 비밀번호로 변경합니다.")
56+
@PatchMapping("/password")
57+
public RsData<?> updatePassword(
58+
@Valid @RequestBody UserUpdatePasswordRequest req
59+
) {
60+
User user = rq.getUser();
61+
userService.updatePassword(user, req);
62+
return RsData.success("비밀번호가 변경되었습니다.");
63+
}
64+
65+
66+
@Operation(summary = "회원 탈퇴", description = "현재 로그인된 사용자를 탈퇴 처리합니다.")
67+
@DeleteMapping("/me")
68+
public RsData<?> deleteMyAccount() {
69+
User user = rq.getUser();
70+
71+
userService.deleteMyAccount(user);
72+
tokenService.removeTokens(user); // refresh/access 토큰 정리
73+
74+
return RsData.success("회원 탈퇴가 완료되었습니다.");
75+
}
1376
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.back.web7_9_codecrete_be.domain.users.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class UserUpdateNicknameRequest {
9+
10+
@NotBlank(message = "닉네임은 필수입니다.")
11+
@Size(min = 2, max = 20, message = "닉네임은 2자 이상 20자 이하입니다.")
12+
private String nickname;
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.back.web7_9_codecrete_be.domain.users.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Pattern;
6+
import lombok.Getter;
7+
8+
@Getter
9+
public class UserUpdatePasswordRequest {
10+
11+
@NotBlank(message = "현재 비밀번호는 필수입니다.")
12+
@Schema(description = "현재 비밀번호", example = "oldPassword1!")
13+
private String currentPassword;
14+
15+
@NotBlank(message = "비밀번호는 필수입니다.")
16+
@Pattern(
17+
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,}$",
18+
message = "비밀번호는 영문, 숫자, 특수문자를 포함한 8자 이상이어야 합니다."
19+
)
20+
@Schema(description = "비밀번호", example = "1234abcd!")
21+
private String newPassword;
22+
}

src/main/java/com/back/web7_9_codecrete_be/domain/users/dto/request/UserUpdateRequest.java

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/main/java/com/back/web7_9_codecrete_be/domain/users/entity/User.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,19 @@ public User(String email,
8282
public void changePassword(String encodedPassword) {
8383
this.password = encodedPassword;
8484
}
85+
86+
public void updateNickname(String nickname) {
87+
this.nickname = nickname;
88+
}
89+
90+
public void updateProfileImage(String profileImage) {
91+
this.profileImage = profileImage;
92+
}
93+
94+
public void softDelete() {
95+
this.isDeleted = true;
96+
this.status = UserStatus.DELETED;
97+
this.deletedDate = LocalDateTime.now();
98+
}
8599
}
86100

src/main/java/com/back/web7_9_codecrete_be/domain/users/entity/UserStatus.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
public enum UserStatus {
44
ACTIVE, // 활성 상태
5+
DELETED, // 삭제 상태
56
SUSPENDED // 정지 상태
67
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,101 @@
11
package com.back.web7_9_codecrete_be.domain.users.service;
22

33

4+
import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;
5+
import com.back.web7_9_codecrete_be.domain.users.dto.request.UserUpdateNicknameRequest;
6+
import com.back.web7_9_codecrete_be.domain.users.dto.request.UserUpdatePasswordRequest;
7+
import com.back.web7_9_codecrete_be.domain.users.dto.response.UserResponse;
8+
import com.back.web7_9_codecrete_be.domain.users.entity.User;
9+
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
10+
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
11+
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
12+
import com.back.web7_9_codecrete_be.global.storage.FileStorageService;
413
import lombok.RequiredArgsConstructor;
14+
import org.springframework.security.crypto.password.PasswordEncoder;
515
import org.springframework.stereotype.Service;
16+
import org.springframework.transaction.annotation.Transactional;
17+
import org.springframework.web.multipart.MultipartFile;
618

719
@Service
820
@RequiredArgsConstructor
21+
@Transactional
922
public class UserService {
23+
private final UserRepository userRepository;
24+
private final FileStorageService fileStorageService;
25+
private final PasswordEncoder passwordEncoder;
26+
private final TokenService tokenService;
27+
28+
// 내 정보 조회
29+
@Transactional(readOnly = true)
30+
public UserResponse getMyInfo(User user) {
31+
validateActiveUser(user);
32+
return UserResponse.from(user);
33+
}
34+
35+
// 닉네임 수정
36+
public UserResponse updateNickname(User user, UserUpdateNicknameRequest req) {
37+
validateActiveUser(user);
38+
String newNickname = req.getNickname();
39+
40+
// 닉네임이 변경되는 경우에만 중복 검사
41+
if (!newNickname.equals(user.getNickname())) {
42+
if (userRepository.existsByNickname(newNickname)) {
43+
throw new BusinessException(UserErrorCode.NICKNAME_DUPLICATED);
44+
}
45+
}
46+
47+
user.updateNickname(req.getNickname());
48+
userRepository.save(user);
49+
return UserResponse.from(user);
50+
}
51+
52+
// 회원 탈퇴
53+
public void deleteMyAccount(User user) {
54+
validateActiveUser(user);
55+
user.softDelete();
56+
userRepository.save(user);
57+
58+
// 로그아웃 처리
59+
tokenService.removeTokens(user);
60+
}
61+
62+
// 프로필 이미지 수정
63+
public String updateProfileImage(User user, MultipartFile file) {
64+
validateActiveUser(user);
65+
66+
// 파일 유효성 검사
67+
if (file == null || file.isEmpty()) {
68+
throw new BusinessException(UserErrorCode.INVALID_PROFILE_IMAGE);
69+
}
70+
71+
String imageUrl = fileStorageService.upload(file);
72+
73+
user.updateProfileImage(imageUrl);
74+
userRepository.save(user);
75+
76+
return imageUrl;
77+
}
78+
79+
// 활성 사용자 검증
80+
private void validateActiveUser(User user) {
81+
if (user.getIsDeleted()) {
82+
throw new BusinessException(UserErrorCode.USER_DELETED);
83+
}
84+
}
85+
86+
// 비밀번호 변경
87+
public void updatePassword(User user, UserUpdatePasswordRequest req) {
88+
validateActiveUser(user);
89+
90+
// 현재 비밀번호 검증
91+
if (!passwordEncoder.matches(req.getCurrentPassword(), user.getPassword())) {
92+
throw new BusinessException(UserErrorCode.INVALID_PASSWORD);
93+
}
94+
95+
user.changePassword(passwordEncoder.encode(req.getNewPassword()));
96+
userRepository.save(user);
97+
98+
// 비밀번호 변경 시 로그아웃 처리
99+
tokenService.removeTokens(user);
100+
}
10101
}

src/main/java/com/back/web7_9_codecrete_be/global/error/code/AuthErrorCode.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,12 @@ public enum AuthErrorCode implements ErrorCode {
1111

1212
// 회원가입 관련
1313
EMAIL_DUPLICATED(HttpStatus.CONFLICT, "A-100", "이미 사용중인 이메일입니다."),
14-
NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "A-101", "이미 사용중인 닉네임입니다."),
15-
EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "A-102","이메일 인증이 완료되지 않았습니다."),
14+
EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "A-101","이메일 인증이 완료되지 않았습니다."),
1615

1716
// 로그인 관련
1817
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "A-110", "존재하지 않는 이메일입니다."),
1918
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "A-111", "비밀번호가 일치하지 않습니다."),
2019
USER_INACTIVE(HttpStatus.FORBIDDEN, "A-112", "현재 비활성화된 계정입니다."),
21-
USER_DELETED(HttpStatus.FORBIDDEN, "A-113", "탈퇴한 사용자는 로그인할 수 없습니다."),
2220

2321
// 권한 관련
2422
UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "A-120", "로그인이 필요합니다."),

0 commit comments

Comments
 (0)