diff --git a/src/main/java/com/server/running_handai/domain/member/controller/MemberController.java b/src/main/java/com/server/running_handai/domain/member/controller/MemberController.java index 467aef2..f1759f4 100644 --- a/src/main/java/com/server/running_handai/domain/member/controller/MemberController.java +++ b/src/main/java/com/server/running_handai/domain/member/controller/MemberController.java @@ -1,5 +1,8 @@ package com.server.running_handai.domain.member.controller; +import com.server.running_handai.domain.member.dto.MemberUpdateRequestDto; +import com.server.running_handai.domain.member.dto.MemberUpdateResponseDto; +import com.server.running_handai.global.oauth.CustomOAuth2User; import com.server.running_handai.global.response.CommonResponse; import com.server.running_handai.global.response.ResponseCode; import com.server.running_handai.domain.member.dto.TokenRequestDto; @@ -9,10 +12,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +@Slf4j +@Validated @RestController @RequiredArgsConstructor @RequestMapping("/api/members") @@ -21,15 +31,67 @@ public class MemberController { private final MemberService memberService; @Operation(summary = "토큰 재발급", - description = "리프래쉬 토큰을 통해 만료된 액세스 토큰을 재발급합니다. 인증에 사용된 리프래시 토큰 역시 액세스 토큰과 함께 재발급됩니다. 로그인은 /oauth2/authorization/{provider}로 요청해주세요.") + description = "리프래쉬 토큰을 통해 만료된 액세스 토큰을 재발급합니다. 인증에 사용된 리프래시 토큰 역시 액세스 토큰과 함께 재발급됩니다. " + + "로그인은 /oauth2/authorization/{provider}로 요청해주세요.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "401", description = "실패 (유효하지 않은 토큰)"), - @ApiResponse(responseCode = "404", description = "실패 (찾을 수 없는 리프래시 토큰)") + @ApiResponse(responseCode = "200", description = "성공 - SUCCESS"), + @ApiResponse(responseCode = "401", description = "실패 (유효하지 않은 토큰) - INVALID_REFRESH_TOKEN"), + @ApiResponse(responseCode = "404", description = "실패 (찾을 수 없는 리프래시 토큰) - REFRESH_TOKEN_NOT_FOUND") }) @PostMapping("/oauth/token") public ResponseEntity> createToken(@RequestBody TokenRequestDto tokenRequestDto) { TokenResponseDto tokenResponseDto = memberService.createToken(tokenRequestDto); return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, tokenResponseDto)); } + + @Operation(summary = "닉네임 중복 여부 조회", + description = "사용자가 수정하려는 닉네임이 중복이 아닌 경우 true, 중복인 경우 false를 응답합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공 - SUCCESS"), + @ApiResponse(responseCode = "401", description = "토큰 인증 필요 - UNAUTHORIZED_ACCESS"), + @ApiResponse(responseCode = "404", description = "실패 (찾을 수 없는 사용자) - MEMBER_NOT_FOUND"), + @ApiResponse(responseCode = "400", description = + "실패 (유효성 검증):
" + + "• 글자수가 2글자 미만, 10글자 초과 - INVALID_NICKNAME_LENGTH
" + + "• 한글, 영문, 숫자 외의 문자가 존재 - INVALID_NICKNAME_FORMAT
" + + "• 현재 사용 중인 닉네임과 동일 - SAME_AS_CURRENT_NICKNAME
" + + "• 공백이나 Null 값으로 호출 - INVALID_INPUT_VALUE" + ), + }) + @GetMapping("/nickname") + public ResponseEntity> checkNicknameDuplicate( + @NotBlank @RequestParam("value") String nickname, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User + ) { + Long memberId = customOAuth2User.getMember().getId(); + log.info("[닉네임 중복 여부 조회] memberId: {} nickname: {}", memberId, nickname); + Boolean result = memberService.checkNicknameDuplicate(memberId, nickname); + return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, result)); + } + + @Operation(summary = "내 정보 수정", + description = "내 정보를 수정합니다. 현재는 닉네임 수정만 제공합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공 - SUCCESS"), + @ApiResponse(responseCode = "401", description = "토큰 인증 필요 - UNAUTHORIZED_ACCESS"), + @ApiResponse(responseCode = "404", description = "실패 (찾을 수 없는 사용자) - MEMBER_NOT_FOUND"), + @ApiResponse(responseCode = "400", description = + "실패 (유효성 검증):
" + + "• 글자수가 2글자 미만, 10글자 초과 - INVALID_NICKNAME_LENGTH
" + + "• 한글, 영문, 숫자 외의 문자가 존재 - INVALID_NICKNAME_FORMAT
" + + "• 현재 사용 중인 닉네임과 동일 - SAME_AS_CURRENT_NICKNAME
" + + "• 공백이나 Null 값으로 호출 - INVALID_INPUT_VALUE" + ), + @ApiResponse(responseCode = "409", description = "실패 (중복된 닉네임) - DUPLICATE_NICKNAME"), + }) + @PatchMapping("/me") + public ResponseEntity> updateMemberInfo( + @RequestBody @Valid MemberUpdateRequestDto memberUpdateRequestDto, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User + ) { + Long memberId = customOAuth2User.getMember().getId(); + log.info("[내 정보 수정] memberId: {}", memberId); + MemberUpdateResponseDto memberUpdateResponseDto = memberService.updateMemberInfo(memberId, memberUpdateRequestDto); + return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, memberUpdateResponseDto)); + } } diff --git a/src/main/java/com/server/running_handai/domain/member/dto/MemberUpdateRequestDto.java b/src/main/java/com/server/running_handai/domain/member/dto/MemberUpdateRequestDto.java new file mode 100644 index 0000000..a78e9f3 --- /dev/null +++ b/src/main/java/com/server/running_handai/domain/member/dto/MemberUpdateRequestDto.java @@ -0,0 +1,9 @@ +package com.server.running_handai.domain.member.dto; + +import jakarta.validation.constraints.NotBlank; + +public record MemberUpdateRequestDto ( + @NotBlank + String nickname +) { +} diff --git a/src/main/java/com/server/running_handai/domain/member/dto/MemberUpdateResponseDto.java b/src/main/java/com/server/running_handai/domain/member/dto/MemberUpdateResponseDto.java new file mode 100644 index 0000000..6c5d6a3 --- /dev/null +++ b/src/main/java/com/server/running_handai/domain/member/dto/MemberUpdateResponseDto.java @@ -0,0 +1,10 @@ +package com.server.running_handai.domain.member.dto; + +public record MemberUpdateResponseDto ( + Long memberId, + String nickname +) { + public static MemberUpdateResponseDto from(Long memberId, String nickname) { + return new MemberUpdateResponseDto(memberId, nickname); + } +} \ No newline at end of file diff --git a/src/main/java/com/server/running_handai/domain/member/entity/Member.java b/src/main/java/com/server/running_handai/domain/member/entity/Member.java index 160cd86..7bf20ce 100644 --- a/src/main/java/com/server/running_handai/domain/member/entity/Member.java +++ b/src/main/java/com/server/running_handai/domain/member/entity/Member.java @@ -60,7 +60,10 @@ public Member(String providerId, String email, String nickname, Provider provide this.role = role; } + // ==== 연관관계 편의 메서드 ==== // public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } + + public void updateNickname(String nickname) { this.nickname = nickname; } } \ No newline at end of file diff --git a/src/main/java/com/server/running_handai/domain/member/repository/MemberRepository.java b/src/main/java/com/server/running_handai/domain/member/repository/MemberRepository.java index 3fd2cfd..c4cdfa0 100644 --- a/src/main/java/com/server/running_handai/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/server/running_handai/domain/member/repository/MemberRepository.java @@ -6,7 +6,18 @@ import java.util.Optional; public interface MemberRepository extends JpaRepository { + /** + * Provider Id로 사용자를 조회합니다. + */ Optional findByProviderId(String providerId); - boolean existsByNickname(String nickname); + + /** + * 리프래시 토큰으로 사용자를 조회합니다. + */ Optional findByRefreshToken(String refreshToken); + + /** + * 닉네임 중복 여부를 확인합니다. + */ + boolean existsByNickname(String nickname); } diff --git a/src/main/java/com/server/running_handai/domain/member/service/MemberService.java b/src/main/java/com/server/running_handai/domain/member/service/MemberService.java index 26b4380..5f7728a 100644 --- a/src/main/java/com/server/running_handai/domain/member/service/MemberService.java +++ b/src/main/java/com/server/running_handai/domain/member/service/MemberService.java @@ -1,5 +1,7 @@ package com.server.running_handai.domain.member.service; +import com.server.running_handai.domain.member.dto.MemberUpdateRequestDto; +import com.server.running_handai.domain.member.dto.MemberUpdateResponseDto; import com.server.running_handai.global.jwt.JwtProvider; import com.server.running_handai.global.oauth.userInfo.OAuth2UserInfo; import com.server.running_handai.global.response.ResponseCode; @@ -10,7 +12,7 @@ import com.server.running_handai.domain.member.entity.Role; import com.server.running_handai.domain.member.repository.MemberRepository; import io.jsonwebtoken.ExpiredJwtException; -import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,7 +26,9 @@ public class MemberService { private final MemberRepository memberRepository; private final JwtProvider jwtProvider; - public static final int NICKNAME_NUMBER = 10; + public static final int NICKNAME_MAX_LENGTH = 10; + public static final int NICKNAME_MIN_LENGTH = 2; + private static final String NICKNAME_PATTERN = "^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9]{2,10}$"; /** * OAuth2 사용자 정보를 기반으로 회원을 생성하거나 기존 회원을 조회합니다. @@ -130,7 +134,7 @@ private String generateRandomNickname() { String animal = animals.get(random.nextInt(animals.size())); int usedLength = adjective.length() + animal.length(); - int remainLength = NICKNAME_NUMBER - usedLength; + int remainLength = NICKNAME_MAX_LENGTH - usedLength; if (remainLength > 0) { // 이미 선택된 형용사, 동물의 자리수를 확인하여, 남은 수를 숫자에 사용 (최소 1자리, 최대 remainLength) @@ -153,4 +157,74 @@ private String generateRandomNickname() { return nickname; } + + /** + * 닉네임 중복 여부를 조회합니다. + * 닉네임 유효성 검증도 함께 수행합니다. + * + * @param memberId 사용자 Id + * @param nickname 검증할 닉네임 + * @return 중복이지 않으면 true, 중복이면 false. + */ + public Boolean checkNicknameDuplicate(Long memberId, String nickname) { + Member member = memberRepository.findById(memberId).orElseThrow(() -> new BusinessException(ResponseCode.MEMBER_NOT_FOUND)); + + // 중복 여부 조회 시 문자 앞, 뒤 공백과 영문 대, 소문자는 무시 (프론트 측에서 처리해서 보내줌) + String newNickname = nickname.trim().toLowerCase(); + String currentNickname = member.getNickname().trim().toLowerCase(); + + return isNicknameValid(newNickname, currentNickname); + } + + /** + * 내 정보를 수정합니다. + * 닉네임 유효성 검증도 함께 수행합니다. + * + * @param memberId 사용자 Id + * @param memberUpdateRequestDto 수정하고 싶은 내 정보 Dto + * @return 수정된 내 정보 Dto (MemberUpdateResponseDto) + */ + @Transactional + public MemberUpdateResponseDto updateMemberInfo(Long memberId, MemberUpdateRequestDto memberUpdateRequestDto) { + Member member = memberRepository.findById(memberId).orElseThrow(() -> new BusinessException(ResponseCode.MEMBER_NOT_FOUND)); + + // 중복 여부 조회 시 문자 앞, 뒤 공백과 영문 대, 소문자는 무시 (프론트 측에서 처리해서 보내줌) + String newNickname = memberUpdateRequestDto.nickname().trim().toLowerCase(); + String currentNickname = member.getNickname().trim().toLowerCase(); + + if (isNicknameValid(newNickname, currentNickname)) { + member.updateNickname(newNickname); + } else { + throw new BusinessException(ResponseCode.DUPLICATE_NICKNAME); + } + + return MemberUpdateResponseDto.from(member.getId(), member.getNickname()); + } + + /** + * 닉네임 유효성을 검증합니다. + * 테스트를 위해 가시성을 완화했습니다. (private -> package-private) + * + * @param newNickname 검증할 닉네임 + * @param currentNickname 사용자의 현재 닉네임 + * @return 사용 가능하면 true, 사용 불가하면 false. + */ + boolean isNicknameValid(String newNickname, String currentNickname) { + // 이미 자신이 사용 중인 닉네임이어서는 안됨 + if (currentNickname.equals(newNickname)) { + throw new BusinessException(ResponseCode.SAME_AS_CURRENT_NICKNAME); + } + + // 닉네임 글자수는 2글자부터 최대 10글자까지 + if (newNickname.length() < NICKNAME_MIN_LENGTH || newNickname.length() > NICKNAME_MAX_LENGTH) { + throw new BusinessException(ResponseCode.INVALID_NICKNAME_LENGTH); + } + + // 닉네임은 한글, 숫자, 영문만 입력할 수 있음 + if (!newNickname.matches(NICKNAME_PATTERN)) { + throw new BusinessException(ResponseCode.INVALID_NICKNAME_FORMAT); + } + + return !memberRepository.existsByNickname(newNickname); + } } \ No newline at end of file diff --git a/src/main/java/com/server/running_handai/global/response/ResponseCode.java b/src/main/java/com/server/running_handai/global/response/ResponseCode.java index b45c9a2..80246ad 100644 --- a/src/main/java/com/server/running_handai/global/response/ResponseCode.java +++ b/src/main/java/com/server/running_handai/global/response/ResponseCode.java @@ -28,6 +28,9 @@ public enum ResponseCode { INVALID_REVIEW_STARS(BAD_REQUEST, "별점은 0.5점 단위여야합니다."), EMPTY_REVIEW_CONTENTS(BAD_REQUEST, "리뷰 내용은 비워둘 수 없습니다"), BAD_REQUEST_STATE_PARAMETER(BAD_REQUEST, "로그인 요청 시 유효한 state 값이 필요합니다."), + INVALID_NICKNAME_LENGTH(BAD_REQUEST, "닉네임은 2글자부터 10글자까지 입력할 수 있습니다."), + INVALID_NICKNAME_FORMAT(BAD_REQUEST, "닉네임은 영문, 한글, 숫자만 입력할 수 있습니다."), + SAME_AS_CURRENT_NICKNAME(BAD_REQUEST, "현재 사용 중인 닉네임과 동일합니다."), // UNAUTHORIZED (401) INVALID_ACCESS_TOKEN(UNAUTHORIZED, "유효하지 않은 액세스 토큰입니다."), @@ -46,14 +49,17 @@ public enum ResponseCode { BOOKMARK_NOT_FOUND(NOT_FOUND, "찾을 수 없는 북마크입니다."), REVIEW_NOT_FOUND(NOT_FOUND, "찾을 수 없는 리뷰입니다."), + // CONFLICT (409) + DUPLICATE_NICKNAME(CONFLICT, "이미 사용 중인 닉네임입니다."), + /** 시스템 및 공통 예외용 에러 코드 */ // BAD_REQUEST (400) ILLEGAL_ARGUMENT(BAD_REQUEST, "잘못된 인자 값입니다."), - METHOD_ARGUMENT_NOT_VALID(BAD_REQUEST, "유효하지 않은 인자 값입니다."), HTTP_MESSAGE_NOT_READABLE(BAD_REQUEST, "잘못된 요청 형식입니다."), MISSING_SERVLET_REQUEST_PARAMETER(BAD_REQUEST, "필수 요청 매개변수가 누락되었습니다."), ARGUMENT_TYPE_MISMATCH(BAD_REQUEST, "요청 매개변수의 타입이 올바르지 않습니다."), OPENAI_RESPONSE_INVALID(BAD_REQUEST, "OPEN AI 응답값이 유효하지 않습니다."), + INVALID_INPUT_VALUE(BAD_REQUEST, "유효하지 않은 입력 값입니다."), // NOT_FOUND (404) RESOURCE_NOT_FOUND(NOT_FOUND, "존재하지 않는 리소스입니다."), diff --git a/src/main/java/com/server/running_handai/global/response/exception/GlobalExceptionHandler.java b/src/main/java/com/server/running_handai/global/response/exception/GlobalExceptionHandler.java index d58ed8c..66c2574 100644 --- a/src/main/java/com/server/running_handai/global/response/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/server/running_handai/global/response/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import com.server.running_handai.global.response.CommonResponse; import com.server.running_handai.global.response.ResponseCode; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -27,10 +28,11 @@ public ResponseEntity> handleCustomException(BusinessException /** * BAD_REQUEST (400) * IllegalArgumentException: 사용자가 값을 잘못 입력한 경우 - * MethodArgumentNotValidException: 전달된 값이 유효하지 않은 경우 + * MethodArgumentNotValidException: 전달된 값이 유효하지 않은 경우 (@Valid) * HttpMessageNotReadableException: 잘못된 형식으로 요청할 경우 * MissingServletRequestParameterException: 필수 요청 매개변수가 누락된 경우 * MethodArgumentTypeMismatchException: 요청 매개변수의 타입 변환을 실패한 경우 + * ConstraintViolationException: 전달된 값이 유효하지 않은 경우 (@Validated) */ @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { @@ -40,7 +42,7 @@ public ResponseEntity> handleIllegalArgumentException(IllegalA @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleMethodArgumentNotValidException( MethodArgumentNotValidException e) { - return getErrorResponse(e, ResponseCode.METHOD_ARGUMENT_NOT_VALID); + return getErrorResponse(e, ResponseCode.INVALID_INPUT_VALUE); } @ExceptionHandler(HttpMessageNotReadableException.class) @@ -61,6 +63,12 @@ public ResponseEntity> handleMethodArgumentTypeMismatchExcepti return getErrorResponse(e, ResponseCode.ARGUMENT_TYPE_MISMATCH); } + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException( + ConstraintViolationException e) { + return getErrorResponse(e, ResponseCode.INVALID_INPUT_VALUE); + } + /** * METHOD_NOT_ALLOWED (405) * HttpRequestMethodNotSupportedException: 잘못된 Http Method를 가지고 요청할 경우 diff --git a/src/test/java/com/server/running_handai/domain/member/service/MemberServiceTest.java b/src/test/java/com/server/running_handai/domain/member/service/MemberServiceTest.java new file mode 100644 index 0000000..3924cee --- /dev/null +++ b/src/test/java/com/server/running_handai/domain/member/service/MemberServiceTest.java @@ -0,0 +1,227 @@ +package com.server.running_handai.domain.member.service; + +import com.server.running_handai.domain.member.dto.MemberUpdateRequestDto; +import com.server.running_handai.domain.member.dto.MemberUpdateResponseDto; +import com.server.running_handai.domain.member.entity.Member; +import com.server.running_handai.domain.member.repository.MemberRepository; +import com.server.running_handai.global.response.exception.BusinessException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static com.server.running_handai.global.response.ResponseCode.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + @InjectMocks + private MemberService memberService; + + @Mock + private MemberRepository memberRepository; + + @Nested + @DisplayName("닉네임 유효성 검증 테스트") + class NicknameValidationTest { + + /** + * [닉네임 유효성 검증] 실패 + * 1. 현재 자신의 닉네임과 동일한 경우 + */ + @Test + @DisplayName("닉네임 유효성 검증 실패 - 본인 닉네임과 동일") + void isNicknameValid_fail_sameAsCurrentNickname() { + // given + String currentNickname = "current"; + String newNickname = "current"; + + // when & then + BusinessException exception = assertThrows(BusinessException.class, () -> memberService.isNicknameValid(newNickname, currentNickname)); + assertThat(exception.getResponseCode()).isEqualTo(SAME_AS_CURRENT_NICKNAME); + } + + /** + * [닉네임 유효성 검증] 실패 + * 2. 글자수가 안맞는 경우 (2글자 ~ 10글자) + */ + @ParameterizedTest + @ValueSource(strings = {"a", "verylongnickname123"}) + @DisplayName("닉네임 유효성 검증 실패 - 글자수가 안맞음") + void isNicknameValid_fail_inValidNicknameLength(String newNickname) { + BusinessException exception = assertThrows(BusinessException.class, () -> memberService.isNicknameValid(newNickname, "current")); + assertThat(exception.getResponseCode()).isEqualTo(INVALID_NICKNAME_LENGTH); + } + + /** + * [닉네임 유효성 검증] 실패 + * 3. 한글, 영문, 숫자 외의 문자가 존재하는 경우 + */ + @ParameterizedTest + @ValueSource(strings = {"hello@", "닉네임!", "test#123"}) + @DisplayName("닉네임 유효성 검증 실패 - 한글, 영문, 숫자 외의 문자 존재") + void isNicknameValid_fail_inValidNicknameFormat(String newNickname) { + BusinessException exception = assertThrows(BusinessException.class, () -> memberService.isNicknameValid(newNickname, "current")); + assertThat(exception.getResponseCode()).isEqualTo(INVALID_NICKNAME_FORMAT); + } + } + + @Nested + @DisplayName("닉네임 중복 여부 조회 테스트") + class CheckNicknameDuplicateTest { + private static final Long MEMBER_ID = 1L; + + // 헬퍼 메서드 + private Member createMockMember(Long memberId) { + Member member = Member.builder().nickname("current").build(); + ReflectionTestUtils.setField(member, "id", memberId); + return member; + } + + /** + * [닉네임 중복 여부 조회] 성공 + * 1. 중복되지 않은 닉네임인 경우 (true 응답) + */ + @Test + @DisplayName("닉네임 중복 확인 성공 - 중복되지 않은 닉네임") + void checkNicknameDuplicate_success_notDuplicateNickname() { + // given + Member member = createMockMember(MEMBER_ID); + given(memberRepository.findById(MEMBER_ID)).willReturn(Optional.of(member)); + + String newNickname = "new"; + given(memberRepository.existsByNickname("new")).willReturn(false); + + // when + Boolean result = memberService.checkNicknameDuplicate(member.getId(), newNickname); + + // then + assertThat(result).isTrue(); + verify(memberRepository).findById(MEMBER_ID); + verify(memberRepository).existsByNickname(newNickname); + } + + /** + * [닉네임 중복 여부 조회] 성공 + * 2. 중복된 닉네임인 경우 (false 응답) + */ + @Test + @DisplayName("닉네임 중복 확인 성공 - 중복된 닉네임") + void checkNicknameDuplicate_success_duplicateNickname() { + // given + Member member = createMockMember(MEMBER_ID); + given(memberRepository.findById(MEMBER_ID)).willReturn(Optional.of(member)); + + String newNickname = "duplicate"; + given(memberRepository.existsByNickname("duplicate")).willReturn(true); + + // when + Boolean result = memberService.checkNicknameDuplicate(member.getId(), newNickname); + + // then + assertThat(result).isFalse(); + verify(memberRepository).findById(MEMBER_ID); + verify(memberRepository).existsByNickname(newNickname); + } + + /** + * [닉네임 중복 여부 조회] 실패 + * 1. Member가 존재하지 않을 경우 + */ + @Test + @DisplayName("닉네임 중복 확인 실패 - 찾을 수 없는 사용자") + void checkNicknameDuplicate_fail_memberNotFound() { + // given + given(memberRepository.findById(MEMBER_ID)).willReturn(Optional.empty()); + + // when, then + BusinessException exception = assertThrows(BusinessException.class, () -> memberService.checkNicknameDuplicate(MEMBER_ID, anyString())); + assertThat(exception.getResponseCode()).isEqualTo(MEMBER_NOT_FOUND); + } + } + + @Nested + @DisplayName("내 정보 수정 테스트") + class UpdateMemberInfoTest { + private static final Long MEMBER_ID = 1L; + + // 헬퍼 메서드 + private Member createMockMember(Long memberId) { + Member member = Member.builder().nickname("current").build(); + ReflectionTestUtils.setField(member, "id", memberId); + return member; + } + + /** + * [내 정보 수정] 성공 + * 1. 수정을 성공한 경우 + */ + @Test + @DisplayName("내 정보 수정 성공") + void updateMemberInfo_success() { + // given + Member member = createMockMember(MEMBER_ID); + given(memberRepository.findById(MEMBER_ID)).willReturn(Optional.of(member)); + + MemberUpdateRequestDto memberUpdateRequestDto = new MemberUpdateRequestDto("new"); + given(memberRepository.existsByNickname("new")).willReturn(false); + + // when + MemberUpdateResponseDto memberUpdateResponseDto = memberService.updateMemberInfo(member.getId(), memberUpdateRequestDto); + + // then + assertThat(memberUpdateResponseDto.nickname()).isEqualTo("new"); + verify(memberRepository).findById(MEMBER_ID); + verify(memberRepository).existsByNickname("new"); + } + + /** + * [내 정보 수정] 실패 + * 1. Member가 존재하지 않을 경우 + */ + @Test + @DisplayName("내 정보 수정 실패 - 찾을 수 없는 사용자") + void updateMemberInfo_fail_memberNotFound() { + // given + given(memberRepository.findById(MEMBER_ID)).willReturn(Optional.empty()); + MemberUpdateRequestDto memberUpdateRequestDto = new MemberUpdateRequestDto("new"); + + // when, then + BusinessException exception = assertThrows(BusinessException.class, () -> memberService.updateMemberInfo(MEMBER_ID, memberUpdateRequestDto)); + assertThat(exception.getResponseCode()).isEqualTo(MEMBER_NOT_FOUND); + } + + /** + * [내 정보 수정] 실패 + * 2. 중복된 닉네임인 경우 + */ + @Test + @DisplayName("내 정보 수정 - 중복된 닉네임") + void updateMemberInfo_fail_duplicateNickname() { + // given + Member member = createMockMember(MEMBER_ID); + given(memberRepository.findById(MEMBER_ID)).willReturn(Optional.of(member)); + + MemberUpdateRequestDto memberUpdateRequestDto = new MemberUpdateRequestDto("duplicate"); + given(memberRepository.existsByNickname("duplicate")).willReturn(true); + + // when, then + BusinessException exception = assertThrows(BusinessException.class, () -> memberService.updateMemberInfo(MEMBER_ID, memberUpdateRequestDto)); + assertThat(exception.getResponseCode()).isEqualTo(DUPLICATE_NICKNAME); + } + } +}