diff --git a/src/main/java/com/depromeet/domain/member/api/MemberController.java b/src/main/java/com/depromeet/domain/member/api/MemberController.java index d86ade861..20e7078a6 100644 --- a/src/main/java/com/depromeet/domain/member/api/MemberController.java +++ b/src/main/java/com/depromeet/domain/member/api/MemberController.java @@ -44,7 +44,7 @@ public ResponseEntity memberUsernameCheck( return ResponseEntity.ok().build(); } - @Operation(summary = "닉네임 중복 체크", description = "닉네임 중복 체크를 진행합니다.") + @Operation(summary = "닉네임 유효성 체크", description = "닉네임 유효성 체크를 진행합니다.") @PostMapping("/check-nickname") public ResponseEntity memberNicknameCheck( @Valid @RequestBody NicknameCheckRequest request) { @@ -54,7 +54,8 @@ public ResponseEntity memberNicknameCheck( @Operation(summary = "닉네임으로 회원 검색", description = "닉네임으로 회원을 검색합니다.") @GetMapping("/search") - public List memberNicknameSearch(@RequestParam String nickname) { + public List memberNicknameSearch( + @RequestParam(required = false) String nickname) { return memberService.searchMemberNickname(nickname); } @@ -76,8 +77,8 @@ public ResponseEntity memberSocialInfoFind() { @Operation(summary = "회원 닉네임 변경", description = "회원 닉네임을 변경합니다.") @PutMapping("/me/nickname") public ResponseEntity memberNicknameUpdate( - @Valid @RequestBody NicknameUpdateRequest reqest) { - memberService.updateMemberNickname(reqest); + @Valid @RequestBody NicknameUpdateRequest request) { + memberService.updateMemberNickname(request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java index 2b3fb6640..f98eaedce 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -62,6 +62,13 @@ public void checkUsername(UsernameCheckRequest request) { @Transactional(readOnly = true) public void checkNickname(NicknameCheckRequest request) { validateNicknameNotDuplicate(request.nickname()); + if (validateNicknameText(request.nickname())) { + throw new CustomException(ErrorCode.MEMBER_INVALID_NICKNAME); + } + } + + private boolean validateNicknameText(String nickname) { + return nickname == null || nickname.trim().isEmpty(); } private void validateNicknameNotDuplicate(String nickname) { @@ -73,8 +80,11 @@ private void validateNicknameNotDuplicate(String nickname) { @Transactional(readOnly = true) public List searchMemberNickname(String nickname) { final Member currentMember = memberUtil.getCurrentMember(); + final String escapingNickname = escapeSpecialCharacters(nickname); + List members = - memberRepository.nicknameSearch(nickname, currentMember.getProfile().getNickname()); + memberRepository.nicknameSearch( + escapingNickname, currentMember.getProfile().getNickname()); List memberRelationBySourceId = memberRelationRepository.findAllBySourceIdAndTargetIn( currentMember.getId(), members); @@ -147,10 +157,10 @@ private void validateSocialInfoNotNull(Member member) { } } - public void updateMemberNickname(NicknameUpdateRequest reqest) { + public void updateMemberNickname(NicknameUpdateRequest request) { final Member currentMember = memberUtil.getCurrentMember(); - validateNicknameNotDuplicate(reqest.nickname()); - currentMember.updateNickname(reqest.nickname()); + validateNicknameNotDuplicate(request.nickname()); + currentMember.updateNickname(escapeSpecialCharacters(request.nickname())); } private ImageFileExtension getImageFileExtension(Profile profile) { @@ -174,4 +184,9 @@ public void updateFcmToken(UpdateFcmTokenRequest updateFcmTokenRequest) { final Member currentMember = memberUtil.getCurrentMember(); currentMember.updateFcmToken(currentMember.getFcmInfo(), updateFcmTokenRequest.fcmToken()); } + + private String escapeSpecialCharacters(String nickname) { + // 여기서 특수문자를 '_'로 대체할 수 있도록 정규표현식을 활용하여 구현 + return nickname == null ? "" : nickname.replaceAll("[^0-9a-zA-Z가-힣 ]", "_"); + } } diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java index 0932908c4..df936de37 100644 --- a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java @@ -2,6 +2,7 @@ import com.depromeet.domain.member.domain.Member; import com.depromeet.domain.member.domain.OauthInfo; +import io.lettuce.core.dynamic.annotation.Param; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -20,6 +21,7 @@ public interface MemberRepository extends JpaRepository { Optional findByProfileNickname(String nickname); @Query( - "SELECT m FROM Member m WHERE m.profile.nickname like %:searchNickname% AND m.profile.nickname != :myNickname") - List nicknameSearch(String searchNickname, String myNickname); + "SELECT m FROM Member m WHERE m.profile.nickname LIKE CONCAT('%', :searchNickname, '%') escape '_' AND m.profile.nickname != :myNickname") + List nicknameSearch( + @Param("searchNickname") String searchNickname, @Param("myNickname") String myNickname); } diff --git a/src/main/java/com/depromeet/domain/member/domain/Profile.java b/src/main/java/com/depromeet/domain/member/domain/Profile.java index bc16641f9..a66ed152c 100644 --- a/src/main/java/com/depromeet/domain/member/domain/Profile.java +++ b/src/main/java/com/depromeet/domain/member/domain/Profile.java @@ -1,13 +1,17 @@ package com.depromeet.domain.member.domain; import jakarta.persistence.Embeddable; +import jakarta.validation.constraints.Pattern; import lombok.*; @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Profile { + + @Pattern(regexp = "[^0-9a-zA-Z가-힣 ]", message = "올바르지 않은 닉네임 표현입니다.") private String nickname; + @Getter private String profileImageUrl; @Builder(access = AccessLevel.PRIVATE) diff --git a/src/main/java/com/depromeet/domain/notification/application/.gitkeep b/src/main/java/com/depromeet/domain/notification/application/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java index 7f23ce60a..f61e1ddbd 100644 --- a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java +++ b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java @@ -25,6 +25,7 @@ public enum ErrorCode { EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."), MEMBER_ALREADY_REGISTERED(HttpStatus.CONFLICT, "이미 가입된 회원입니다."), MEMBER_ALREADY_NICKNAME(HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."), + MEMBER_INVALID_NICKNAME(HttpStatus.BAD_REQUEST, "올바르지 않는 닉네임입니다."), MEMBER_ALREADY_DELETED(HttpStatus.NOT_FOUND, "이미 탈퇴한 회원입니다."), PASSWORD_NOT_MATCHES(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), ID_TOKEN_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, "ID 토큰 검증에 실패했습니다."), diff --git a/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java b/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java index 8d5143b80..319a4fa05 100644 --- a/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java +++ b/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java @@ -181,6 +181,64 @@ void setUp() { assertFalse(responses.contains(currentMember.getProfile().getNickname())); } + @Test + void 검색_키워드에_퍼센트_키워드가_들어왔을_때_NPE_무시_처리한다() { + // given + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모", "도모 이미지 URL"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모 바보", "testImageUrl"))); + + String searchNickname = "%"; + + // when + List responses = + memberService.searchMemberNickname(searchNickname); + + // then + assertEquals(0, responses.size()); + } + + @Test + void 검색_키워드에_공백에_따른_처리만_허용한다() { + // given + memberRepository.save(Member.createNormalMember(Profile.createProfile("바보", "도모 얼굴"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도 모", "도모 이미지 URL"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모 바보", "testImageUrl"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile(" 도모", "도모 잘생김"))); + String searchNickname = " "; + + // when + List responses = + memberService.searchMemberNickname(searchNickname); + + // then + assertEquals(3, responses.size()); + } + + @Test + void 검색_키워드에_특수문자_입력_시_정규식_예외처리를_한다() { + // given + memberRepository.save( + Member.createNormalMember(Profile.createProfile("#도모", "도모 이미지 URL"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모 바보#", "testImageUrl"))); + + memberRepository.save( + Member.createNormalMember(Profile.createProfile("#도모 바보#", "testImageUrl"))); + String searchNickname = "#"; + + // when + List responses = + memberService.searchMemberNickname(searchNickname); + + // then + assertEquals(0, responses.size()); + } + @Test void 정렬조건은_일치하는경우_먼저보여주고_나머지는_사전순에_따른다() { // given