Skip to content

Commit 3b47c29

Browse files
authored
Merge pull request #46 from Sportize/refactor/user
✨ Feat: 사용자 정보 수정, 조회 기능 구현 + 사용자 정보 캐시 무효화
2 parents 762fa5b + 6c392e7 commit 3b47c29

10 files changed

Lines changed: 236 additions & 8 deletions

File tree

src/main/java/com/be/sportizebe/domain/user/controller/UserController.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.be.sportizebe.domain.user.controller;
22

33
import com.be.sportizebe.domain.user.dto.request.SignUpRequest;
4+
import com.be.sportizebe.domain.user.dto.request.UpdateProfileRequest;
45
import com.be.sportizebe.domain.user.dto.response.ProfileImageResponse;
56
import com.be.sportizebe.domain.user.dto.response.SignUpResponse;
7+
import com.be.sportizebe.domain.user.dto.response.UpdateProfileResponse;
8+
import com.be.sportizebe.domain.user.dto.response.UserInfoResponse;
69
import com.be.sportizebe.domain.user.service.UserServiceImpl;
710
import com.be.sportizebe.global.response.BaseResponse;
811
import com.be.sportizebe.global.cache.dto.UserAuthInfo;
@@ -42,4 +45,23 @@ public ResponseEntity<BaseResponse<ProfileImageResponse>> uploadProfileImage(
4245
ProfileImageResponse response = userService.uploadProfileImage(userAuthInfo.getId(), file);
4346
return ResponseEntity.ok(BaseResponse.success("프로필 사진 업로드 성공", response));
4447
}
48+
49+
@PutMapping("/profile")
50+
@Operation(summary = "프로필 수정", description = "닉네임, 한줄소개를 수정합니다.")
51+
public ResponseEntity<BaseResponse<UpdateProfileResponse>> updateProfile(
52+
@AuthenticationPrincipal UserAuthInfo userAuthInfo,
53+
@RequestBody @Valid UpdateProfileRequest request
54+
) {
55+
UpdateProfileResponse response = userService.updateProfile(userAuthInfo.getId(), request);
56+
return ResponseEntity.ok(BaseResponse.success("프로필 수정 성공", response));
57+
}
58+
59+
@GetMapping("/me")
60+
@Operation(summary = "내 정보 조회", description = "현재 로그인한 사용자의 정보를 조회합니다.")
61+
public ResponseEntity<BaseResponse<UserInfoResponse>> getMyInfo(
62+
@AuthenticationPrincipal UserAuthInfo userAuthInfo
63+
) {
64+
UserInfoResponse response = userService.getUserInfo(userAuthInfo.getId());
65+
return ResponseEntity.ok(BaseResponse.success("사용자 정보 조회 성공", response));
66+
}
4567
}

src/main/java/com/be/sportizebe/domain/user/dto/request/SignUpRequest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.be.sportizebe.domain.user.dto.request;
22

3+
import com.be.sportizebe.domain.user.entity.Gender;
34
import io.swagger.v3.oas.annotations.media.Schema;
45
import jakarta.validation.constraints.Email;
56
import jakarta.validation.constraints.NotBlank;
@@ -18,6 +19,13 @@ public record SignUpRequest(
1819

1920
@Schema(description = "닉네임", example = "스포티")
2021
@NotBlank(message = "사용할 닉네임을 입력해주세요.")
21-
String nickName
22+
String nickName,
23+
24+
@Schema(description = "성별", example = "MALE")
25+
Gender gender,
26+
27+
@Schema(description = "전화번호", example = "010-xxxx-xxxx")
28+
@NotBlank(message = "전화번호를 입력해주세요")
29+
String phoneNumber
2230
) {
2331
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.be.sportizebe.domain.user.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Size;
6+
7+
@Schema(title = "UpdateProfileRequest DTO", description = "프로필 수정 요청")
8+
public record UpdateProfileRequest(
9+
@Schema(description = "닉네임", example = "새로운닉네임")
10+
@NotBlank(message = "닉네임은 필수입니다")
11+
@Size(min = 2, max = 20, message = "닉네임은 2~20자 사이여야 합니다")
12+
String nickname,
13+
14+
@Schema(description = "한줄 소개", example = "안녕하세요! 축구를 좋아합니다.")
15+
@Size(max = 100, message = "한줄 소개는 100자 이내여야 합니다")
16+
String introduce
17+
) {}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.be.sportizebe.domain.user.dto.response;
2+
3+
import com.be.sportizebe.domain.user.entity.User;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.Builder;
6+
7+
@Builder
8+
@Schema(title = "UpdateProfileResponse DTO", description = "프로필 수정 응답")
9+
public record UpdateProfileResponse(
10+
@Schema(description = "사용자 ID", example = "1")
11+
Long userId,
12+
13+
@Schema(description = "닉네임", example = "새로운닉네임")
14+
String nickname,
15+
16+
@Schema(description = "한줄 소개", example = "안녕하세요! 축구를 좋아합니다.")
17+
String introduce
18+
) {
19+
public static UpdateProfileResponse from(User user) {
20+
return UpdateProfileResponse.builder()
21+
.userId(user.getId())
22+
.nickname(user.getNickname())
23+
.introduce(user.getIntroduce())
24+
.build();
25+
}
26+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.be.sportizebe.domain.user.dto.response;
2+
3+
import com.be.sportizebe.domain.user.entity.Gender;
4+
import com.be.sportizebe.domain.user.entity.SportType;
5+
import com.be.sportizebe.domain.user.entity.User;
6+
import com.be.sportizebe.global.cache.dto.UserAuthInfo;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import lombok.Builder;
9+
10+
import java.util.List;
11+
12+
@Builder
13+
@Schema(title = "UserInfoResponse DTO", description = "사용자 정보 조회 응답")
14+
public record UserInfoResponse(
15+
@Schema(description = "사용자 ID", example = "1")
16+
Long userId,
17+
18+
@Schema(description = "이메일(아이디)", example = "test@example.com")
19+
String username,
20+
21+
@Schema(description = "닉네임", example = "축구왕")
22+
String nickname,
23+
24+
@Schema(description = "한줄 소개", example = "안녕하세요!")
25+
String introduce,
26+
27+
@Schema(description = "프로필 이미지 URL")
28+
String profileImage,
29+
30+
@Schema(description = "성별", example = "MALE")
31+
Gender gender,
32+
33+
@Schema(description = "전화번호", example = "010-1234-5678")
34+
String phoneNumber,
35+
36+
@Schema(description = "관심 종목")
37+
List<SportType> interestType
38+
) {
39+
public static UserInfoResponse from(User user) { // DB 조회
40+
return UserInfoResponse.builder()
41+
.userId(user.getId())
42+
.username(user.getUsername())
43+
.nickname(user.getNickname())
44+
.introduce(user.getIntroduce())
45+
.profileImage(user.getProfileImage())
46+
.gender(user.getGender())
47+
.phoneNumber(user.getPhoneNumber())
48+
.interestType(user.getInterestType())
49+
.build();
50+
}
51+
52+
public static UserInfoResponse from(UserAuthInfo userAuthInfo) { // 캐시메모리 조회
53+
return UserInfoResponse.builder()
54+
.userId(userAuthInfo.getId())
55+
.username(userAuthInfo.getUsername())
56+
.nickname(userAuthInfo.getNickname())
57+
.introduce(userAuthInfo.getIntroduce())
58+
.profileImage(userAuthInfo.getProfileImage())
59+
.gender(userAuthInfo.getGender())
60+
.phoneNumber(userAuthInfo.getPhoneNumber())
61+
.interestType(userAuthInfo.getInterestType())
62+
.build();
63+
}
64+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.be.sportizebe.domain.user.entity;
2+
3+
public enum Gender {
4+
MALE,
5+
FEMALE
6+
}

src/main/java/com/be/sportizebe/domain/user/entity/User.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ public class User extends BaseTimeEntity {
4646

4747
private String profileImage; // 프로필 사진 URL
4848

49+
private String introduce; // 한줄 소개
50+
51+
@Column(nullable = false)
52+
@Enumerated(EnumType.STRING)
53+
private Gender gender; // 성별
54+
55+
@Column(nullable = false, unique = true)
56+
private String phoneNumber; // 전화번호
57+
58+
/*@Column(nullable = false)
59+
private String address; // 주소*/
60+
4961
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
5062
@Builder.Default
5163
private List<Post> posts = new ArrayList<>(); // 작성한 게시글 목록
@@ -57,4 +69,9 @@ public void updateRefreshToken(String refreshToken) {
5769
public void updateProfileImage(String profileImage) {
5870
this.profileImage = profileImage;
5971
}
72+
73+
public void updateProfile(String nickname, String introduce) {
74+
this.nickname = nickname;
75+
this.introduce = introduce;
76+
}
6077
}

src/main/java/com/be/sportizebe/domain/user/service/UserService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.be.sportizebe.domain.user.service;
22

33
import com.be.sportizebe.domain.user.dto.request.SignUpRequest;
4+
import com.be.sportizebe.domain.user.dto.request.UpdateProfileRequest;
45
import com.be.sportizebe.domain.user.dto.response.ProfileImageResponse;
56
import com.be.sportizebe.domain.user.dto.response.SignUpResponse;
7+
import com.be.sportizebe.domain.user.dto.response.UpdateProfileResponse;
8+
import com.be.sportizebe.domain.user.dto.response.UserInfoResponse;
69
import org.springframework.web.multipart.MultipartFile;
710

811
public interface UserService {
@@ -12,4 +15,10 @@ public interface UserService {
1215

1316
// 프로필 사진 업로드
1417
ProfileImageResponse uploadProfileImage(Long userId, MultipartFile file);
18+
19+
// 프로필 수정 (닉네임, 한줄소개)
20+
UpdateProfileResponse updateProfile(Long userId, UpdateProfileRequest request);
21+
22+
// 사용자 정보 조회
23+
UserInfoResponse getUserInfo(Long userId);
1524
}

src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.be.sportizebe.domain.user.service;
22

33
import com.be.sportizebe.domain.user.dto.request.SignUpRequest;
4+
import com.be.sportizebe.domain.user.dto.request.UpdateProfileRequest;
45
import com.be.sportizebe.domain.user.dto.response.ProfileImageResponse;
56
import com.be.sportizebe.domain.user.dto.response.SignUpResponse;
7+
import com.be.sportizebe.domain.user.dto.response.UpdateProfileResponse;
8+
import com.be.sportizebe.domain.user.dto.response.UserInfoResponse;
69
import com.be.sportizebe.domain.user.entity.Role;
710
import com.be.sportizebe.domain.user.entity.User;
811
import com.be.sportizebe.domain.user.exception.UserErrorCode;
912
import com.be.sportizebe.domain.user.repository.UserRepository;
13+
import com.be.sportizebe.global.cache.service.UserCacheService;
1014
import com.be.sportizebe.global.exception.CustomException;
1115
import com.be.sportizebe.global.s3.enums.PathName;
1216
import com.be.sportizebe.global.s3.service.S3Service;
@@ -24,6 +28,7 @@ public class UserServiceImpl implements UserService {
2428

2529
private final UserRepository userRepository;
2630
private final PasswordEncoder passwordEncoder;
31+
private final UserCacheService userCacheService;
2732
private final S3Service s3Service;
2833

2934
@Override
@@ -46,6 +51,8 @@ public SignUpResponse signUp(SignUpRequest request) {
4651
.username(request.username())
4752
.password(encodedPassword)
4853
.nickname(request.nickName())
54+
.phoneNumber(request.phoneNumber())
55+
.gender(request.gender())
4956
.role(Role.USER)
5057
.build();
5158

@@ -72,8 +79,44 @@ public ProfileImageResponse uploadProfileImage(Long userId, MultipartFile file)
7279
// 사용자 프로필 이미지 URL 업데이트
7380
user.updateProfileImage(profileImageUrl);
7481

82+
// 프로필 이미지가 캐시에 포함되어 있으므로 캐시 무효화
83+
userCacheService.evictUserAuthInfo(userId);
84+
7585
log.info("사용자 프로필 이미지 업로드 완료: userId={}, url={}", userId, profileImageUrl);
7686

7787
return ProfileImageResponse.from(profileImageUrl);
7888
}
89+
90+
@Override
91+
@Transactional
92+
public UpdateProfileResponse updateProfile(Long userId, UpdateProfileRequest request) {
93+
User user = userRepository.findById(userId)
94+
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
95+
96+
// 닉네임 중복 체크 (자신의 닉네임이 아닌 경우만)
97+
if (!user.getNickname().equals(request.nickname())
98+
&& userRepository.existsByNickname(request.nickname())) {
99+
throw new CustomException(UserErrorCode.DUPLICATE_NICKNAME);
100+
}
101+
102+
user.updateProfile(request.nickname(), request.introduce());
103+
104+
// 닉네임이 UserAuthInfo에 포함되어 있으므로 캐시 무효화
105+
userCacheService.evictUserAuthInfo(userId);
106+
107+
log.info("사용자 프로필 수정 완료: userId={}", userId);
108+
109+
return UpdateProfileResponse.from(user);
110+
}
111+
112+
@Override
113+
public UserInfoResponse getUserInfo(Long userId) {
114+
// 캐시에서 사용자 정보 조회 (캐시 미스 시 DB 조회 후 캐싱)
115+
var userAuthInfo = userCacheService.findUserAuthInfoById(userId);
116+
if (userAuthInfo == null) {
117+
throw new CustomException(UserErrorCode.USER_NOT_FOUND);
118+
}
119+
120+
return UserInfoResponse.from(userAuthInfo);
121+
}
79122
}
Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,48 @@
11
package com.be.sportizebe.global.cache.dto;
22

3+
import com.be.sportizebe.domain.user.entity.Gender;
34
import com.be.sportizebe.domain.user.entity.Role;
5+
import com.be.sportizebe.domain.user.entity.SportType;
46
import com.be.sportizebe.domain.user.entity.User;
57
import lombok.AllArgsConstructor;
8+
import lombok.Builder;
69
import lombok.Getter;
710
import lombok.NoArgsConstructor;
811

912
import java.io.Serializable;
13+
import java.util.List;
1014

1115
/**
1216
* JWT 인증 필터에서 사용할 캐시용 사용자 정보 DTO
1317
* User 엔티티의 연관관계(posts 등)로 인한 직렬화 문제를 방지
18+
* 민감정보(password, refreshToken) 제외
1419
*/
1520
@Getter
21+
@Builder
1622
@NoArgsConstructor
1723
@AllArgsConstructor
18-
public class UserAuthInfo implements Serializable { // Serializable : 직렬화 가능하다라는 마커 표시
24+
public class UserAuthInfo implements Serializable {
1925
private Long id;
2026
private String username;
2127
private String nickname;
2228
private Role role;
29+
private String profileImage;
30+
private String introduce;
31+
private Gender gender;
32+
private String phoneNumber;
33+
private List<SportType> interestType;
2334

2435
public static UserAuthInfo from(User user) {
25-
return new UserAuthInfo(
26-
user.getId(),
27-
user.getUsername(),
28-
user.getNickname(),
29-
user.getRole()
30-
);
36+
return UserAuthInfo.builder()
37+
.id(user.getId())
38+
.username(user.getUsername())
39+
.nickname(user.getNickname())
40+
.role(user.getRole())
41+
.profileImage(user.getProfileImage())
42+
.introduce(user.getIntroduce())
43+
.gender(user.getGender())
44+
.phoneNumber(user.getPhoneNumber())
45+
.interestType(user.getInterestType())
46+
.build();
3147
}
3248
}

0 commit comments

Comments
 (0)