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
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/auth/reissue",
"/auth/devices",
"/auth/reissue/devices",
"/users/nickname/check",
"/ws-dodo"
).permitAll()
.requestMatchers(
Expand Down Expand Up @@ -164,4 +165,4 @@ private void sendErrorResponse(HttpServletResponse response, UserErrorCode error

response.getWriter().write(objectMapper.writeValueAsString(errorBody));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.dodo.backend.user.dto.request.UserRequest.UserUpdateRequest;
import com.dodo.backend.user.dto.request.UserRequest.WithdrawalRequest;
import com.dodo.backend.user.dto.response.UserResponse;
import com.dodo.backend.user.dto.response.UserResponse.NicknameCheckResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserInfoResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserRegisterResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserUpdateResponse;
Expand Down Expand Up @@ -36,6 +37,38 @@ public class UserController {

private final UserService userService;

/**
* 회원가입 전 입력한 닉네임이 이미 사용 중인지 확인합니다.
*
* @param nickname 중복 여부를 확인할 닉네임
* @return 닉네임과 중복 여부가 포함된 응답 DTO
*/
@Operation(summary = "닉네임 중복 확인", description = "입력한 닉네임의 중복 여부를 확인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "닉네임 중복 확인이 완료되었습니다.",
content = @Content(schema = @Schema(implementation = NicknameCheckResponse.class))),
@ApiResponse(responseCode = "400", description = "닉네임 형식이 올바르지 않습니다.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "400 Bad Request", value = "{\"status\": 400, \"message\": \"닉네임 형식이 올바르지 않습니다.\"}"))),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없습니다.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "404 Not Found", value = "{\"status\": 404, \"message\": \"사용자를 찾을 수 없습니다.\"}"))),
@ApiResponse(responseCode = "500", description = "서버 내부 오류가 발생했습니다.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "500 Internal Server Error", value = "{\"status\": 500, \"message\": \"서버 내부 오류가 발생했습니다.\"}")))
})
@GetMapping("/nickname/check")
public ResponseEntity<NicknameCheckResponse> checkNicknameDuplication(
@RequestParam String nickname) {

log.info("닉네임 중복 확인 요청 - nickname: {}", nickname);

return ResponseEntity.ok(userService.checkNicknameDuplication(nickname));
}

/**
* 회원가입 프로세스의 마지막 단계로, 추가 정보를 입력받아 계정을 활성화합니다.
* <p>
Expand Down Expand Up @@ -291,4 +324,4 @@ public ResponseEntity<String> updateNotification(

return ResponseEntity.ok("알림 수신 설정을 성공적으로 변경했습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,31 @@ public static UserUpdateResponse toDto(User user, String message, String nicknam
.build();
}
}
}

/**
* 닉네임 중복 확인 결과를 반환하는 응답 DTO입니다.
*/
@Getter
@Builder
@AllArgsConstructor
@Schema(description = "닉네임 중복 확인 응답")
public static class NicknameCheckResponse {

@Schema(description = "응답 메시지", example = "닉네임 중복 확인이 완료되었습니다.")
private String message;

@Schema(description = "확인한 닉네임", example = "도도")
private String nickname;

@Schema(description = "닉네임 중복 여부", example = "false")
private Boolean duplicated;

public static NicknameCheckResponse toDto(String nickname, Boolean duplicated) {
return NicknameCheckResponse.builder()
.message("닉네임 중복 확인이 완료되었습니다.")
.nickname(nickname)
.duplicated(duplicated)
.build();
}
}
}
11 changes: 10 additions & 1 deletion src/main/java/com/dodo/backend/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.dodo.backend.user.dto.request.UserRequest.UserRegisterRequest;
import com.dodo.backend.user.dto.request.UserRequest.UserUpdateRequest;
import com.dodo.backend.user.dto.response.UserResponse.UserInfoResponse;
import com.dodo.backend.user.dto.response.UserResponse.NicknameCheckResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserRegisterResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserUpdateResponse;
import com.dodo.backend.user.entity.User;
Expand Down Expand Up @@ -77,6 +78,14 @@ public interface UserService {
*/
void updateNotification(UUID userId, Boolean enabled);

/**
* 닉네임 중복 여부를 확인합니다.
*
* @param nickname 확인할 닉네임
* @return 닉네임 중복 확인 결과 응답 DTO
*/
NicknameCheckResponse checkNicknameDuplication(String nickname);

/**
* ID로 사용자 엔티티를 조회합니다.
* <p>
Expand All @@ -88,4 +97,4 @@ public interface UserService {
*/
User getUserById(UUID userId);

}
}
36 changes: 35 additions & 1 deletion src/main/java/com/dodo/backend/user/service/UserServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.dodo.backend.user.dto.request.UserRequest;
import com.dodo.backend.user.dto.request.UserRequest.UserRegisterRequest;
import com.dodo.backend.user.dto.response.UserResponse.UserInfoResponse;
import com.dodo.backend.user.dto.response.UserResponse.NicknameCheckResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserRegisterResponse;
import com.dodo.backend.user.dto.response.UserResponse.UserUpdateResponse;
import com.dodo.backend.user.entity.User;
Expand Down Expand Up @@ -36,6 +37,10 @@
@Slf4j
public class UserServiceImpl implements UserService {

private static final String NICKNAME_PATTERN = "^[가-힣a-zA-Z0-9 ]*$";
private static final int NICKNAME_MIN_LENGTH = 2;
private static final int NICKNAME_MAX_LENGTH = 10;

private final UserRepository userRepository;
private final UserMapper userMapper;
private final JwtTokenProvider jwtTokenProvider;
Expand Down Expand Up @@ -281,6 +286,35 @@ public void updateNotification(UUID userId, Boolean enabled) {
userMapper.updateNotificationStatus(userId, enabled);
}

/**
* 회원가입 전 입력한 닉네임의 형식을 검증하고 중복 여부를 확인합니다.
* <p>
* 닉네임은 2자 이상 10자 이하이며, 한글, 영문, 숫자, 공백만 사용할 수 있습니다.
*
* @param nickname 중복 여부를 확인할 닉네임
* @return 닉네임과 중복 여부가 포함된 응답 DTO
* @throws UserException 닉네임이 비어 있거나 형식이 올바르지 않은 경우
*/
@Transactional(readOnly = true)
@Override
public NicknameCheckResponse checkNicknameDuplication(String nickname) {
if (nickname == null || nickname.isBlank()) {
throw new UserException(INVALID_REQUEST);
}

if (nickname.length() < NICKNAME_MIN_LENGTH || nickname.length() > NICKNAME_MAX_LENGTH) {
throw new UserException(INVALID_REQUEST);
}

if (!nickname.matches(NICKNAME_PATTERN)) {
throw new UserException(INVALID_REQUEST);
}

boolean duplicated = userRepository.existsByNickname(nickname);

return NicknameCheckResponse.toDto(nickname, duplicated);
}
Comment on lines +300 to +316
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

🔍 닉네임 유효성 검증 추가 필요

현재 checkNicknameDuplication 메서드에서는 입력받은 nickname에 대한 유효성 검증(Null 및 공백 여부 등)이 누락되어 있습니다.

UserController의 OpenAPI 문서(@ApiResponse)에는 **"400 닉네임 형식이 올바르지 않습니다."**라는 응답이 정의되어 있지만, 실제 코드에서는 검증 로직이 없어 빈 문자열("")이나 공백만으로 구성된 닉네임도 그대로 DB를 조회하게 됩니다.

또한, 회원가입 시 적용되는 닉네임 형식 규칙(예: 길이 제한, 허용 문자 등)과 동일한 검증을 이 API에서도 수행해야 합니다. 그렇지 않으면 사용자가 중복 확인 시에는 "사용 가능"으로 안내받았으나, 실제 회원가입 단계에서 형식 오류로 가입이 실패하는 비일관적인 사용자 경험(UX)이 발생할 수 있습니다.

개선 사항:

  • nicknamenull이거나 공백(isBlank())인 경우 UserException(INVALID_REQUEST)을 발생시켜 안전하게 예외 처리합니다.
    public NicknameCheckResponse checkNicknameDuplication(String nickname) {
        if (nickname == null || nickname.isBlank()) {
            throw new UserException(INVALID_REQUEST);
        }
        boolean duplicated = userRepository.existsByNickname(nickname);

        return NicknameCheckResponse.toDto(nickname, duplicated);
    }


/**
* ID로 사용자 엔티티 조회 (예외 처리 포함)
*/
Expand All @@ -290,4 +324,4 @@ public User getUserById(UUID userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserException(USER_NOT_FOUND));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.dodo.backend.user.controller;

import com.dodo.backend.user.dto.response.UserResponse.NicknameCheckResponse;
import com.dodo.backend.user.service.UserService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;

/**
* {@link UserController}의 HTTP 요청 처리 로직을 검증하는 테스트 클래스입니다.
*/
@ExtendWith(MockitoExtension.class)
class UserControllerTest {

@Mock
private UserService userService;

/**
* 닉네임 중복 확인 요청 시 200 상태 코드와 중복 확인 결과를 반환하는지 검증합니다.
*/
@Test
@DisplayName("닉네임 중복 확인 성공")
void checkNicknameDuplicationSuccessTest() {
//given
UserController userController = new UserController(userService);
String nickname = "도도";
NicknameCheckResponse serviceResponse = NicknameCheckResponse.toDto(nickname, false);

given(userService.checkNicknameDuplication(nickname)).willReturn(serviceResponse);

//when
ResponseEntity<NicknameCheckResponse> response = userController.checkNicknameDuplication(nickname);

//then
assertThat(response.getStatusCode().value()).isEqualTo(200);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getNickname()).isEqualTo(nickname);
assertThat(response.getBody().getDuplicated()).isFalse();

verify(userService).checkNicknameDuplication(nickname);
}
}
Loading
Loading