diff --git a/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java b/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java index 005b694..6c3644d 100644 --- a/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java +++ b/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java @@ -40,10 +40,11 @@ public class SecurityConfig { }; private static final Map AUTH_WHITE_LIST = Map.of( + "/v*/auth/login", POST, + "/v*/auth/kakao/login", POST, "/v*/users", POST, "/v*/users/existence", GET, - "/v*/auth/login", POST, - "/v*/auth/kakao/login", POST + "/v*/users/email", GET ); @Bean diff --git a/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java index b0eaef1..657e68b 100644 --- a/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java +++ b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java @@ -37,6 +37,7 @@ public enum CustomExceptionType { USER_PHONE_DUPLICATION(2203, "이미 사용 중인 전화번호입니다."), USER_KAKAO_UID_DUPLICATION(2204, "이미 가입한 계정입니다."), USER_NOT_FOUND_BY_KAKAO_UID(2205, "일치하는 회원을 찾을 수 없습니다."), + USER_NOT_FOUND_BY_PHONE(2206, "일치하는 회원을 찾을 수 없습니다."), KAKAO_CLIENT(10000, "카카오 서버와의 통신 중 오류가 발생했습니다."), ; diff --git a/src/main/java/com/ajou/hertz/common/validator/PhoneNumber.java b/src/main/java/com/ajou/hertz/common/validator/PhoneNumber.java index b4b1ed8..e891837 100644 --- a/src/main/java/com/ajou/hertz/common/validator/PhoneNumber.java +++ b/src/main/java/com/ajou/hertz/common/validator/PhoneNumber.java @@ -1,6 +1,7 @@ package com.ajou.hertz.common.validator; -import java.lang.annotation.ElementType; +import static java.lang.annotation.ElementType.*; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -15,7 +16,7 @@ *

* {@code null} elements are considered valid. */ -@Target(ElementType.FIELD) +@Target({FIELD, PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneNumberValidator.class) public @interface PhoneNumber { diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserControllerV1.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserControllerV1.java index 08f3a79..5c7e14f 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserControllerV1.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserControllerV1.java @@ -13,8 +13,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.ajou.hertz.common.validator.PhoneNumber; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; +import com.ajou.hertz.domain.user.dto.response.UserEmailResponse; import com.ajou.hertz.domain.user.dto.response.UserExistenceResponse; import com.ajou.hertz.domain.user.dto.response.UserResponse; import com.ajou.hertz.domain.user.service.UserCommandService; @@ -28,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; @Tag(name = "유저 관련 API") @@ -55,6 +58,25 @@ public UserExistenceResponse getExistenceOfUserByEmailV1_1( return new UserExistenceResponse(existence); } + @Operation( + summary = "전화번호로 유저 이메일 찾기", + description = "특정 유저를 식별할 수 있는 정보(전화번호)를 받아 일치하는 유저의 이메일을 조회합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", description = "[2206] 전화번호에 해당하는 유저를 찾을 수 없는 경우", content = @Content) + }) + @GetMapping(value = "/email", headers = API_MINOR_VERSION_HEADER_NAME + "=" + 1) + public UserEmailResponse getUserEmailByPhoneV1_1( + @Parameter( + description = "이메일을 찾고자 하는 유저의 전화번호", + example = "01012345678" + ) @RequestParam @NotBlank @PhoneNumber String phone + ) { + UserDto userDto = userQueryService.getDtoByPhone(phone); + return new UserEmailResponse(userDto.getEmail()); + } + @Operation( summary = "회원 등록", description = "회원을 등록합니다." diff --git a/src/main/java/com/ajou/hertz/domain/user/dto/response/UserEmailResponse.java b/src/main/java/com/ajou/hertz/domain/user/dto/response/UserEmailResponse.java new file mode 100644 index 0000000..035a374 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/user/dto/response/UserEmailResponse.java @@ -0,0 +1,16 @@ +package com.ajou.hertz.domain.user.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class UserEmailResponse { + + @Schema(description = "이메일", example = "example@mail.com") + private String email; +} diff --git a/src/main/java/com/ajou/hertz/domain/user/exception/UserNotFoundByPhoneException.java b/src/main/java/com/ajou/hertz/domain/user/exception/UserNotFoundByPhoneException.java new file mode 100644 index 0000000..e726634 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/user/exception/UserNotFoundByPhoneException.java @@ -0,0 +1,10 @@ +package com.ajou.hertz.domain.user.exception; + +import com.ajou.hertz.common.exception.NotFoundException; +import com.ajou.hertz.common.exception.constant.CustomExceptionType; + +public class UserNotFoundByPhoneException extends NotFoundException { + public UserNotFoundByPhoneException(String phone) { + super(CustomExceptionType.USER_NOT_FOUND_BY_PHONE, phone); + } +} diff --git a/src/main/java/com/ajou/hertz/domain/user/repository/UserRepository.java b/src/main/java/com/ajou/hertz/domain/user/repository/UserRepository.java index 091acfd..516cead 100644 --- a/src/main/java/com/ajou/hertz/domain/user/repository/UserRepository.java +++ b/src/main/java/com/ajou/hertz/domain/user/repository/UserRepository.java @@ -12,6 +12,8 @@ public interface UserRepository extends JpaRepository { Optional findByKakaoUid(String kakaoUid); + Optional findByPhone(String phone); + boolean existsByEmail(String email); boolean existsByKakaoUid(String kakaoUid); diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java index 62d635a..a512eaf 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java @@ -9,6 +9,7 @@ import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.exception.UserNotFoundByEmailException; import com.ajou.hertz.domain.user.exception.UserNotFoundByIdException; +import com.ajou.hertz.domain.user.exception.UserNotFoundByPhoneException; import com.ajou.hertz.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -68,6 +69,18 @@ public UserDto getDtoByEmail(String email) { return UserDto.from(getByEmail(email)); } + /** + * 전화번호로 user 정보를 조회한다. + * + * @param phone 조회하고자 하는 user의 전화번호 + * @return 조회한 유저 정보가 담긴 dto + * @throws UserNotFoundByPhoneException 일치하는 유저를 찾지 못한 경우 + */ + public UserDto getDtoByPhone(String phone) { + User user = userRepository.findByPhone(phone).orElseThrow(() -> new UserNotFoundByPhoneException(phone)); + return UserDto.from(user); + } + /** * 전달된 email을 사용 중인 회원의 존재 여부를 조회한다. * diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerV1Test.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerV1Test.java index cf790df..7517076 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerV1Test.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerV1Test.java @@ -90,6 +90,40 @@ public void securitySetUp() throws Exception { verifyEveryMocksShouldHaveNoMoreInteractions(); } + @Test + void 전화번호가_주어지고_주어진_전화번호를_사용_중인_회원의_이메일을_조회한다() throws Exception { + // given + String phone = "01012345678"; + UserDto expectedResult = createUserDto(); + given(userQueryService.getDtoByPhone(phone)).willReturn(expectedResult); + + // when & then + mvc.perform( + get("/v1/users/email") + .header(API_MINOR_VERSION_HEADER_NAME, 1) + .queryParam("phone", phone) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("email").value(expectedResult.getEmail())); + then(userQueryService).should().getDtoByPhone(phone); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + + @Test + void 전화번호가_주어지고_주어진_전화번호를_사용_중인_회원의_이메일을_조회한다_전달된_전화번호가_잘못된_형식인_경우_에러가_발생한다() throws Exception { + // given + String phone = "12345"; + + // when & then + mvc.perform( + get("/v1/users/email") + .header(API_MINOR_VERSION_HEADER_NAME, 1) + .queryParam("phone", phone) + ) + .andExpect(status().isUnprocessableEntity()); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + @Test void 주어진_회원_정보로_신규_회원을_등록한다() throws Exception { // given diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserQueryServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserQueryServiceTest.java index f4e4bb6..e0c6271 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserQueryServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserQueryServiceTest.java @@ -21,6 +21,7 @@ import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.exception.UserNotFoundByEmailException; import com.ajou.hertz.domain.user.exception.UserNotFoundByIdException; +import com.ajou.hertz.domain.user.exception.UserNotFoundByPhoneException; import com.ajou.hertz.domain.user.repository.UserRepository; import com.ajou.hertz.domain.user.service.UserQueryService; @@ -34,6 +35,25 @@ class UserQueryServiceTest { @Mock private UserRepository userRepository; + @Test + void 카카오_유저_ID가_주어지고_주어진_카카오_유저_ID로_유저를_조회하면_조회된_유저_정보가_담긴_Optional_DTO가_반환된다() throws Exception { + // given + String kakaoUid = "12345"; + User expectedResult = createUser(1L, "test@mail.com", kakaoUid, "01012345678"); + given(userRepository.findByKakaoUid(kakaoUid)).willReturn(Optional.of(expectedResult)); + + // when + Optional actualResult = sut.findDtoByKakaoUid(kakaoUid); + + // then + then(userRepository).should().findByKakaoUid(kakaoUid); + verifyEveryMocksShouldHaveNoMoreInteractions(); + assertThat(actualResult).isNotEmpty(); + assertThat(actualResult.get()) + .hasFieldOrPropertyWithValue("id", expectedResult.getId()) + .hasFieldOrPropertyWithValue("kakaoUid", expectedResult.getKakaoUid()); + } + @Test void 유저_id가_주어지고_주어진_id로_유저를_조회하면_조회된_유저_정보가_반환된다() throws Exception { // given @@ -70,7 +90,7 @@ class UserQueryServiceTest { void 이메일이_주어지고_주어진_이메일로_유저를_조회하면_조회된_유저_정보가_반환된다() throws Exception { // given String email = "test@mail.com"; - User expectedResult = createUser(1L, email, "1234"); + User expectedResult = createUser(1L, email, "1234", "01012345678"); given(userRepository.findByEmail(email)).willReturn(Optional.of(expectedResult)); // when @@ -100,22 +120,36 @@ class UserQueryServiceTest { } @Test - void 카카오_유저_ID가_주어지고_주어진_카카오_유저_ID로_유저를_조회하면_조회된_Optional_유저_정보가_반환된다() throws Exception { + void 전화번호가_주어지고_주어진_전화번호로_유저를_조회하면_조회된_유저_정보가_반환된다() throws Exception { // given - String kakaoUid = "12345"; - User expectedResult = createUser(1L, "test@mail.com", kakaoUid); - given(userRepository.findByKakaoUid(kakaoUid)).willReturn(Optional.of(expectedResult)); + String phone = "01012345678"; + User expectedResult = createUser(1L, "test@mail.com", "1234", phone); + given(userRepository.findByPhone(phone)).willReturn(Optional.of(expectedResult)); // when - Optional actualResult = sut.findDtoByKakaoUid(kakaoUid); + UserDto actualResult = sut.getDtoByPhone(phone); // then - then(userRepository).should().findByKakaoUid(kakaoUid); + then(userRepository).should().findByPhone(phone); verifyEveryMocksShouldHaveNoMoreInteractions(); - assertThat(actualResult).isNotEmpty(); - assertThat(actualResult.get()) + assertThat(actualResult) .hasFieldOrPropertyWithValue("id", expectedResult.getId()) - .hasFieldOrPropertyWithValue("kakaoUid", expectedResult.getKakaoUid()); + .hasFieldOrPropertyWithValue("phone", expectedResult.getPhone()); + } + + @Test + void 전화번호가_주어지고_주어진_전화번호로_유저를_조회한다_만약_일치하는_유저가_없다면_예외가_발생한다() { + // given + String phone = "01012345678"; + given(userRepository.findByPhone(phone)).willReturn(Optional.empty()); + + // when + Throwable t = catchThrowable(() -> sut.getDtoByPhone(phone)); + + // then + then(userRepository).should().findByPhone(phone); + verifyEveryMocksShouldHaveNoMoreInteractions(); + assertThat(t).isInstanceOf(UserNotFoundByPhoneException.class); } @Test @@ -170,7 +204,7 @@ private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userRepository).shouldHaveNoMoreInteractions(); } - private User createUser(Long id, String email, String kakaoUid) throws Exception { + private User createUser(Long id, String email, String kakaoUid, String phone) throws Exception { Constructor userConstructor = User.class.getDeclaredConstructor( Long.class, Set.class, String.class, String.class, String.class, String.class, LocalDate.class, Gender.class, String.class, String.class @@ -185,12 +219,12 @@ private User createUser(Long id, String email, String kakaoUid) throws Exception "https://user-default-profile-image-url", LocalDate.of(2024, 1, 1), Gender.ETC, - "01012345678", + phone, null ); } private User createUser(Long id) throws Exception { - return createUser(id, "test@mail.com", "12345"); + return createUser(id, "test@mail.com", "12345", "01012345678"); } } \ No newline at end of file