diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberApi.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberApi.java index a13b436c..795f63ea 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberApi.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberApi.java @@ -5,6 +5,7 @@ import kr.ac.kookmin.stream.api.app.AppApiUser; import kr.ac.kookmin.stream.api.app.core.member.request.FcmTokenUpdateRequest; import kr.ac.kookmin.stream.api.app.core.member.request.MemberSignUpRequest; +import kr.ac.kookmin.stream.api.app.core.member.request.PhoneNumberUpdateRequest; import kr.ac.kookmin.stream.api.common.dto.ApiResponse; import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; import kr.ac.kookmin.stream.common.CommonErrorCode; @@ -13,7 +14,7 @@ /** * 학생 앱 회원 API의 문서 명세. 구현은 {@link AppMemberController}가 맡는다. */ -@Tag(name = "회원", description = "학생 앱 회원가입·FCM 토큰 등록") +@Tag(name = "회원", description = "학생 앱 회원가입·FCM 토큰 등록·전화번호 변경") public interface AppMemberApi { /** 회원가입. 로그인 응답의 signUpRequired가 true인 회원이 전화번호와 약관 동의 여부를 등록한다. */ @@ -38,4 +39,13 @@ public interface AppMemberApi { @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) @ApiErrorCode(type = MemberErrorCode.class, codes = {"MEMBER_NOT_FOUND"}) ApiResponse updateFcmToken(AppApiUser apiUser, FcmTokenUpdateRequest request); + + /** 전화번호 변경. 로그인한 회원이 자기 전화번호를 바꾼다. */ + @Operation(summary = "전화번호 변경", + description = """ + 로그인한 회원의 전화번호를 바꾼다. 전화번호는 하이픈을 넣어도 되고 빼도 된다. + 지금 쓰는 번호와 같으면 바꾸지 않고 성공으로 응답한다. 다른 회원이 쓰는 번호로는 바꿀 수 없다.""") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = MemberErrorCode.class, codes = {"MEMBER_NOT_FOUND", "PHONE_NUMBER_ALREADY_EXISTS"}) + ApiResponse updatePhoneNumber(AppApiUser apiUser, PhoneNumberUpdateRequest request); } diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberController.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberController.java index 67d0ae93..1cfe25e3 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberController.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/AppMemberController.java @@ -4,6 +4,7 @@ import kr.ac.kookmin.stream.api.app.AppApiUser; import kr.ac.kookmin.stream.api.app.core.member.request.FcmTokenUpdateRequest; import kr.ac.kookmin.stream.api.app.core.member.request.MemberSignUpRequest; +import kr.ac.kookmin.stream.api.app.core.member.request.PhoneNumberUpdateRequest; import kr.ac.kookmin.stream.api.common.dto.ApiResponse; import kr.ac.kookmin.stream.member.domain.member.service.MemberService; import lombok.RequiredArgsConstructor; @@ -39,4 +40,14 @@ public ApiResponse updateFcmToken( memberService.updateFcmToken(apiUser.userId(), request.fcmToken()); return ApiResponse.success(); } + + @Override + @PatchMapping("/me/phone-number") + public ApiResponse updatePhoneNumber( + AppApiUser apiUser, + @Valid @RequestBody PhoneNumberUpdateRequest request + ) { + memberService.updatePhoneNumber(apiUser.userId(), request.toCommand()); + return ApiResponse.success(); + } } diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/request/PhoneNumberUpdateRequest.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/request/PhoneNumberUpdateRequest.java new file mode 100644 index 00000000..363291a4 --- /dev/null +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/core/member/request/PhoneNumberUpdateRequest.java @@ -0,0 +1,17 @@ +package kr.ac.kookmin.stream.api.app.core.member.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberPhoneNumberUpdateCommand; + +public record PhoneNumberUpdateRequest( + // 가입 때 받은 번호와 형식이 달라지지 않도록 MemberSignUpRequest와 같은 규칙으로 검증한다 + @NotBlank(message = "전화번호를 입력해 주세요.") + @Pattern(regexp = "^010-?\\d{4}-?\\d{4}$", message = "전화번호 형식이 올바르지 않습니다.") + String phoneNumber +) { + + public MemberPhoneNumberUpdateCommand toCommand() { + return new MemberPhoneNumberUpdateCommand(phoneNumber); + } +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java index 158bc599..3f666e53 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java @@ -68,6 +68,15 @@ public boolean hasPhoneNumber() { return phoneNumber != null; } + // 회원이 바꾼 전화번호로 갱신한다. 바뀐 값이 있으면 true를 돌려준다 + public boolean updatePhoneNumber(String phoneNumber) { + if (Objects.equals(this.phoneNumber, phoneNumber)) { + return false; + } + this.phoneNumber = phoneNumber; + return true; + } + // 앱이 Firebase에서 받은 등록 토큰으로 바꾼다. 바뀐 값이 있으면 true를 돌려준다 public boolean updateFcmToken(String fcmToken) { if (Objects.equals(this.fcmToken, fcmToken)) { diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberPhoneNumberUpdateCommand.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberPhoneNumberUpdateCommand.java new file mode 100644 index 00000000..4e85bc69 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberPhoneNumberUpdateCommand.java @@ -0,0 +1,11 @@ +package kr.ac.kookmin.stream.member.domain.member.domain; + +/** + * 전화번호 변경 요청. 전화번호는 하이픈을 지운 숫자만 담는다. 중복 검사와 저장이 같은 형식을 쓰게 하기 위함이다. + */ +public record MemberPhoneNumberUpdateCommand(String phoneNumber) { + + public MemberPhoneNumberUpdateCommand { + phoneNumber = phoneNumber.replace("-", ""); + } +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java index 39d1a0fd..b0cded3e 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; import kr.ac.kookmin.stream.member.domain.member.domain.Member; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberPhoneNumberUpdateCommand; import kr.ac.kookmin.stream.member.domain.member.domain.MemberProfileCommand; import kr.ac.kookmin.stream.member.domain.member.domain.MemberSignUpCommand; @@ -16,4 +17,5 @@ public interface MemberService { Member signUp(Long memberId, MemberSignUpCommand command); boolean isSignedUp(Long memberId); void updateFcmToken(Long memberId, String fcmToken); + void updatePhoneNumber(Long memberId, MemberPhoneNumberUpdateCommand command); } diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java index f6bdf2d2..16babadf 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java @@ -9,6 +9,7 @@ import kr.ac.kookmin.stream.member.domain.member.domain.Department; import kr.ac.kookmin.stream.member.domain.member.domain.Member; import kr.ac.kookmin.stream.member.domain.member.domain.MemberErrorCode; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberPhoneNumberUpdateCommand; import kr.ac.kookmin.stream.member.domain.member.domain.MemberProfileCommand; import kr.ac.kookmin.stream.member.domain.member.domain.MemberSignUpCommand; import kr.ac.kookmin.stream.member.domain.member.repository.MemberRepository; @@ -95,6 +96,16 @@ public void updateFcmToken(Long memberId, String fcmToken) { } } + @Override + @Transactional + public void updatePhoneNumber(Long memberId, MemberPhoneNumberUpdateCommand command) { + Member member = getById(memberId); + validatePhoneNumberNotDuplicated(member, command.phoneNumber()); + if (member.updatePhoneNumber(command.phoneNumber())) { + memberRepository.save(member); + } + } + // 전화번호와 필수 약관 동의가 모두 있어야 가입을 마친 것으로 본다 private boolean isSignedUp(Member member) { return member.hasPhoneNumber() && memberTermService.hasAgreedRequiredTerms(member.getId()); @@ -106,7 +117,7 @@ private void validateNotSignedUp(Member member) { } } - // 필수 약관이 늘어 다시 가입하는 회원은 자기 번호를 그대로 보낼 수 있으므로 같은 번호면 중복 검사를 건너뛴다 + // 필수 약관이 늘어 다시 가입하거나 같은 번호로 변경을 요청하는 회원은 자기 번호를 그대로 보낼 수 있으므로 같은 번호면 중복 검사를 건너뛴다 private void validatePhoneNumberNotDuplicated(Member member, String phoneNumber) { if (phoneNumber.equals(member.getPhoneNumber())) { return;