Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'com.resend:resend-java:4.1.1'
implementation 'com.googlecode.libphonenumber:libphonenumber:9.0.9'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private String generateToken(User user, Date expiration) {
.subject(user.getEmail())
.id(String.valueOf(user.getId()))
.claim(JwtConstants.ROLE, user.getRole().name())
.claim(JwtConstants.TOKEN_VERSION, user.getTokenVersion())
.issuedAt(now)
.expiration(expiration)
.signWith(secretKey, Jwts.SIG.HS256)
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/project/flipnote/common/util/PhoneUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package project.flipnote.common.util;

import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PhoneUtil {

private static final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();

public static String normalize(String phone) {
if (phone == null) {
return null;
}

try {
Phonenumber.PhoneNumber phoneNumber = phoneUtil.parse(phone, "KR");
return phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
} catch (NumberParseException e) {
throw new IllegalStateException("전화번호 정규화에 실패하였습니다. phone: " + phone, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

import java.util.Objects;

import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import project.flipnote.common.validation.annotation.ValidPhone;

public class PhoneConstraintValidator implements ConstraintValidator<ValidPhone, String> {

private static final String PHONE_PATTERN = "^010-\\d{4}-\\d{4}$";
private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();

@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
if (Objects.isNull(phone)) {
return true;
}

return phone.matches(PHONE_PATTERN);
try {
Phonenumber.PhoneNumber number = phoneUtil.parse(phone, "KR");
return phoneUtil.isValidNumber(number);
} catch (NumberParseException e) {
return false;
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/project/flipnote/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -14,6 +15,8 @@
import project.flipnote.common.security.dto.UserAuth;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserRegisterResponse;
import project.flipnote.user.model.UserUpdateRequest;
import project.flipnote.user.model.UserUpdateResponse;
import project.flipnote.user.service.UserService;

@RequiredArgsConstructor
Expand All @@ -34,4 +37,13 @@ public ResponseEntity<Void> unregister(@AuthenticationPrincipal UserAuth userAut
userService.unregister(userAuth.userId());
return ResponseEntity.noContent().build();
}

@PutMapping
public ResponseEntity<UserUpdateResponse> update(
@AuthenticationPrincipal UserAuth userAuth,
@Valid @RequestBody UserUpdateRequest req
) {
UserUpdateResponse res = userService.update(userAuth.userId(), req);
return ResponseEntity.ok(res);
}
}
7 changes: 7 additions & 0 deletions src/main/java/project/flipnote/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,11 @@ public void unregister() {
public void increaseTokenVersion() {
this.tokenVersion++;
}

public void update(String nickname, String phone, boolean smsAgree, String profileImageUrl) {
this.nickname = nickname;
this.phone = phone;
this.smsAgree = smsAgree;
this.profileImageUrl = profileImageUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import project.flipnote.common.util.PhoneUtil;
import project.flipnote.common.validation.annotation.ValidPassword;
import project.flipnote.common.validation.annotation.ValidPhone;

Expand All @@ -28,7 +29,7 @@ public record UserRegisterRequest(
String profileImageUrl
) {

public String getCleanedPhone() {
return phone == null ? null : phone.replaceAll("-", "");
public String getNormalizedPhone() {
return PhoneUtil.normalize(phone);
}
}
25 changes: 25 additions & 0 deletions src/main/java/project/flipnote/user/model/UserUpdateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package project.flipnote.user.model;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import project.flipnote.common.util.PhoneUtil;
import project.flipnote.common.validation.annotation.ValidPhone;

public record UserUpdateRequest(

@NotEmpty
String nickname,

@ValidPhone
String phone,

@NotNull
Boolean smsAgree,

String profileImageUrl
) {

public String getNormalizedPhone() {
return PhoneUtil.normalize(phone);
}
}
18 changes: 18 additions & 0 deletions src/main/java/project/flipnote/user/model/UserUpdateResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package project.flipnote.user.model;

import project.flipnote.user.entity.User;

public record UserUpdateResponse(
Long userId,
String nickname,
String phone,
Boolean smsAgree,
String profileImageUrl
) {

public static UserUpdateResponse from(User user) {
return new UserUpdateResponse(
user.getId(), user.getNickname(), user.getPhone(), user.isSmsAgree(), user.getProfileImageUrl()
);
}
}
18 changes: 17 additions & 1 deletion src/main/java/project/flipnote/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import project.flipnote.user.exception.UserErrorCode;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserRegisterResponse;
import project.flipnote.user.model.UserUpdateRequest;
import project.flipnote.user.model.UserUpdateResponse;
import project.flipnote.user.repository.UserRepository;

@RequiredArgsConstructor
Expand All @@ -30,7 +32,7 @@ public class UserService {
@Transactional
public UserRegisterResponse register(UserRegisterRequest req) {
String email = req.email();
String phone = req.getCleanedPhone();
String phone = req.getNormalizedPhone();

validateEmailDuplicate(email);
validatePhoneDuplicate(phone);
Expand Down Expand Up @@ -61,6 +63,20 @@ public void unregister(Long userId) {
tokenVersionRedisRepository.deleteTokenVersion(userId);
}

@Transactional
public UserUpdateResponse update(Long userId, UserUpdateRequest req) {
User user = findActiveUserById(userId);

String phone = req.getNormalizedPhone();
if (!Objects.equals(user.getPhone(), phone)) {
validatePhoneDuplicate(phone);
}

user.update(req.nickname(), phone, req.smsAgree(), req.profileImageUrl());

return UserUpdateResponse.from(user);
}

private User findActiveUserById(Long userId) {
return userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE)
.orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
Expand Down

This file was deleted.

85 changes: 84 additions & 1 deletion src/test/java/project/flipnote/user/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand All @@ -26,6 +25,8 @@
import project.flipnote.user.exception.UserErrorCode;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserRegisterResponse;
import project.flipnote.user.model.UserUpdateRequest;
import project.flipnote.user.model.UserUpdateResponse;
import project.flipnote.user.repository.UserRepository;

@DisplayName("회원 서비스 단위 테스트")
Expand Down Expand Up @@ -172,4 +173,86 @@ void fail_userNotFound() {
assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND);
}
}

@DisplayName("회원 정보 수정 테스트")
@Nested
class Update {

@DisplayName("성공")
@Test
void success() {
User user = UserFixture.createActiveUser();
UserUpdateRequest req = new UserUpdateRequest(
"새로운닉네임", "010-9876-5432", true, "new/image.jpg"
);
String normalizedPhone = req.getNormalizedPhone();

given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user));
given(userRepository.existsByPhone(normalizedPhone)).willReturn(false);

UserUpdateResponse res = userService.update(user.getId(), req);

assertThat(res.userId()).isEqualTo(user.getId());
assertThat(res.nickname()).isEqualTo(req.nickname());
assertThat(res.phone()).isEqualTo(normalizedPhone);
assertThat(res.smsAgree()).isEqualTo(req.smsAgree());
assertThat(res.profileImageUrl()).isEqualTo(req.profileImageUrl());

verify(userRepository, times(1)).findByIdAndStatus(anyLong(), any(UserStatus.class));
verify(userRepository, times(1)).existsByPhone(anyString());
}

@DisplayName("동일한 전화번호로 수정 시 성공")
@Test
void success_withSamePhone() {
User user = UserFixture.createActiveUser();
UserUpdateRequest req = new UserUpdateRequest(
"새로운닉네임", user.getPhone(), true, "new/image.jpg"
);
String normalizedPhone = req.getNormalizedPhone();

given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user));

UserUpdateResponse res = userService.update(user.getId(), req);

assertThat(res.userId()).isEqualTo(user.getId());
assertThat(res.nickname()).isEqualTo(req.nickname());
assertThat(res.phone()).isEqualTo(normalizedPhone);
assertThat(res.smsAgree()).isEqualTo(req.smsAgree());
assertThat(res.profileImageUrl()).isEqualTo(req.profileImageUrl());

verify(userRepository, never()).existsByPhone(anyString());
}

@DisplayName("존재하지 않는 회원 수정 시 예외 발생")
@Test
void fail_userNotFound() {
UserUpdateRequest req = new UserUpdateRequest(
"새로운닉네임", "010-9876-5432", true, "new/image.jpg"
);

given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.empty());

BizException exception = assertThrows(BizException.class, () -> userService.update(99L, req));

assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND);
}

@DisplayName("중복된 전화번호로 수정 시 예외 발생")
@Test
void fail_duplicatePhone() {
User user = UserFixture.createActiveUser();
UserUpdateRequest req = new UserUpdateRequest(
"새로운닉네임", "010-9999-9999", true, "new/image.jpg"
);
String duplicatePhone = req.getNormalizedPhone();

given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user));
given(userRepository.existsByPhone(duplicatePhone)).willReturn(true);

BizException exception = assertThrows(BizException.class, () -> userService.update(user.getId(), req));

assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.DUPLICATE_PHONE);
}
}
}
Loading