Repository navigation
[FEAT] 닉네임 중복 확인 API 구현 #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
95be1bd
fix: backingTrack 패키지명 잔재 import 경로 수정 (develop 미반영으로 재발) (#95)
p1001q 91fcc24
feat: 닉네임 중복 확인 API 구현 (GET /api/users/verify-nickname) (#95)
p1001q f72bb29
fix: verify-nickname 파라미터 누락 시 500 대신 NICKNAME_REQUIRED(400) 응답하도록 수정…
p1001q 5f954e7
feat: verify-nickname 응답에 화면 안내 문구(message) 추가 (담당 화면 명세 08-1 반영) (#95)
p1001q File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
35 changes: 35 additions & 0 deletions
35
src/main/java/com/mr/domain/user/controller/UserController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.mr.domain.user.controller; | ||
|
|
||
| import com.mr.domain.user.dto.res.UserResponseDTO; | ||
| import com.mr.domain.user.service.UserService; | ||
| import com.mr.global.apipayload.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/users") | ||
| @Tag(name = "사용자", description = "인증 불필요한 사용자 관련 API") | ||
| public class UserController { | ||
|
|
||
| private final UserService userService; | ||
|
|
||
| @Operation( | ||
| summary = "닉네임 중복 확인", | ||
| description = "닉네임이 형식 규칙(한글/영문/숫자 2~10자)을 만족하는지, 이미 사용 중인지 확인합니다. " | ||
| + "인증 없이 호출 가능합니다." | ||
| ) | ||
| @GetMapping("/verify-nickname") | ||
| public ApiResponse<UserResponseDTO.NicknameCheckResponse> verifyNickname( | ||
| @Parameter(description = "확인할 닉네임", example = "김뮤즈") | ||
| @RequestParam(required = false) String nickname | ||
| ) { | ||
| return ApiResponse.onSuccess(userService.checkNicknameAvailable(nickname)); | ||
| } | ||
| } |
20 changes: 20 additions & 0 deletions
20
src/main/java/com/mr/domain/user/dto/res/UserResponseDTO.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.mr.domain.user.dto.res; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
|
|
||
| public class UserResponseDTO { | ||
|
|
||
| @Builder | ||
| @Schema(description = "닉네임 중복 확인 응답") | ||
| public record NicknameCheckResponse( | ||
| @Schema(description = "확인한 닉네임", example = "김뮤즈") | ||
| String nickname, | ||
|
|
||
| @Schema(description = "사용 가능 여부 (true: 사용 가능, false: 이미 사용 중)", example = "true") | ||
| boolean available, | ||
|
|
||
| @Schema(description = "화면에 바로 표시할 안내 문구", example = "사용 가능한 닉네임입니다.") | ||
| String message | ||
| ) {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.mr.domain.user.service; | ||
|
|
||
| import com.mr.domain.user.dto.res.UserResponseDTO; | ||
| import com.mr.domain.user.entity.User; | ||
| import com.mr.domain.user.repository.UserRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class UserService { | ||
|
|
||
| private static final String NICKNAME_AVAILABLE_MESSAGE = "사용 가능한 닉네임입니다."; | ||
| private static final String NICKNAME_TAKEN_MESSAGE = "사용 중인 닉네임입니다."; | ||
|
|
||
| private final UserRepository userRepository; | ||
|
|
||
| public UserResponseDTO.NicknameCheckResponse checkNicknameAvailable(String nickname) { | ||
| String trimmedNickname = nickname == null ? null : nickname.trim(); | ||
| User.validateNicknameFormat(trimmedNickname); | ||
|
|
||
| boolean available = !userRepository.existsByNickname(trimmedNickname); | ||
|
|
||
| return UserResponseDTO.NicknameCheckResponse.builder() | ||
| .nickname(trimmedNickname) | ||
| .available(available) | ||
| .message(available ? NICKNAME_AVAILABLE_MESSAGE : NICKNAME_TAKEN_MESSAGE) | ||
| .build(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 3 additions & 3 deletions
6
src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
src/test/java/com/mr/domain/user/controller/UserControllerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package com.mr.domain.user.controller; | ||
|
|
||
| import static org.mockito.ArgumentMatchers.eq; | ||
| import static org.mockito.BDDMockito.given; | ||
| import static org.mockito.BDDMockito.willThrow; | ||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
|
||
| import com.mr.domain.user.dto.res.UserResponseDTO; | ||
| import com.mr.domain.user.exception.UserErrorStatus; | ||
| import com.mr.domain.user.service.UserService; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import com.mr.global.apipayload.handler.GlobalExceptionHandler; | ||
| 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.test.web.servlet.MockMvc; | ||
| import org.springframework.test.web.servlet.setup.MockMvcBuilders; | ||
|
|
||
| @ExtendWith(MockitoExtension.class) | ||
| class UserControllerTest { | ||
|
|
||
| private MockMvc mockMvc; | ||
|
|
||
| @Mock | ||
| private UserService userService; | ||
|
|
||
| private MockMvc setUp() { | ||
| return MockMvcBuilders.standaloneSetup(new UserController(userService)) | ||
| .setControllerAdvice(new GlobalExceptionHandler()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("GET /api/users/verify-nickname - nickname 파라미터를 정상적으로 넘기면 200") | ||
| void verifyNickname_success() throws Exception { | ||
| mockMvc = setUp(); | ||
|
|
||
| UserResponseDTO.NicknameCheckResponse response = UserResponseDTO.NicknameCheckResponse.builder() | ||
| .nickname("김뮤즈") | ||
| .available(true) | ||
| .message("사용 가능한 닉네임입니다.") | ||
| .build(); | ||
| given(userService.checkNicknameAvailable("김뮤즈")).willReturn(response); | ||
|
|
||
| mockMvc.perform(get("/api/users/verify-nickname").param("nickname", "김뮤즈")) | ||
| .andExpect(status().isOk()) | ||
| .andExpect(jsonPath("$.isSuccess").value(true)) | ||
| .andExpect(jsonPath("$.data.nickname").value("김뮤즈")) | ||
| .andExpect(jsonPath("$.data.available").value(true)) | ||
| .andExpect(jsonPath("$.data.message").value("사용 가능한 닉네임입니다.")); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("GET /api/users/verify-nickname - nickname 파라미터 자체가 누락되면 400 NICKNAME_REQUIRED로 응답한다 (500 아님)") | ||
| void verifyNickname_missingParam_returns400NicknameRequired() throws Exception { | ||
| mockMvc = setUp(); | ||
|
|
||
| willThrow(new GeneralException(UserErrorStatus.NICKNAME_REQUIRED)) | ||
| .given(userService).checkNicknameAvailable(eq(null)); | ||
|
|
||
| mockMvc.perform(get("/api/users/verify-nickname")) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value("USER_400_01")); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("GET /api/users/verify-nickname - nickname이 빈 문자열이면 400 NICKNAME_REQUIRED로 응답한다") | ||
| void verifyNickname_blankParam_returns400NicknameRequired() throws Exception { | ||
| mockMvc = setUp(); | ||
|
|
||
| willThrow(new GeneralException(UserErrorStatus.NICKNAME_REQUIRED)) | ||
| .given(userService).checkNicknameAvailable(" "); | ||
|
|
||
| mockMvc.perform(get("/api/users/verify-nickname").param("nickname", " ")) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value("USER_400_01")); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("GET /api/users/verify-nickname - 형식 위반이면 400 NICKNAME_INVALID_FORMAT으로 응답한다") | ||
| void verifyNickname_invalidFormat_returns400InvalidFormat() throws Exception { | ||
| mockMvc = setUp(); | ||
|
|
||
| willThrow(new GeneralException(UserErrorStatus.NICKNAME_INVALID_FORMAT)) | ||
| .given(userService).checkNicknameAvailable("김@뮤즈"); | ||
|
|
||
| mockMvc.perform(get("/api/users/verify-nickname").param("nickname", "김@뮤즈")) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value("USER_400_02")); | ||
| } | ||
| } |
84 changes: 84 additions & 0 deletions
84
src/test/java/com/mr/domain/user/service/UserServiceTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package com.mr.domain.user.service; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
| import static org.mockito.BDDMockito.given; | ||
| import static org.mockito.Mockito.never; | ||
| import static org.mockito.Mockito.verify; | ||
|
|
||
| import com.mr.domain.user.dto.res.UserResponseDTO; | ||
| import com.mr.domain.user.exception.UserErrorStatus; | ||
| import com.mr.domain.user.repository.UserRepository; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.extension.ExtendWith; | ||
| import org.mockito.InjectMocks; | ||
| import org.mockito.Mock; | ||
| import org.mockito.junit.jupiter.MockitoExtension; | ||
|
|
||
| @ExtendWith(MockitoExtension.class) | ||
| class UserServiceTest { | ||
|
|
||
| @Mock | ||
| private UserRepository userRepository; | ||
|
|
||
| @InjectMocks | ||
| private UserService userService; | ||
|
|
||
| @Test | ||
| @DisplayName("checkNicknameAvailable - 아무도 안 쓰는 닉네임이면 available=true를 반환한다") | ||
| void checkNicknameAvailable_notTaken_returnsAvailableTrue() { | ||
| given(userRepository.existsByNickname("김뮤즈")).willReturn(false); | ||
|
|
||
| UserResponseDTO.NicknameCheckResponse response = userService.checkNicknameAvailable("김뮤즈"); | ||
|
|
||
| assertThat(response.nickname()).isEqualTo("김뮤즈"); | ||
| assertThat(response.available()).isTrue(); | ||
| assertThat(response.message()).isEqualTo("사용 가능한 닉네임입니다."); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("checkNicknameAvailable - 이미 사용 중인 닉네임이면 available=false를 반환한다") | ||
| void checkNicknameAvailable_alreadyTaken_returnsAvailableFalse() { | ||
| given(userRepository.existsByNickname("김뮤즈")).willReturn(true); | ||
|
|
||
| UserResponseDTO.NicknameCheckResponse response = userService.checkNicknameAvailable("김뮤즈"); | ||
|
|
||
| assertThat(response.available()).isFalse(); | ||
| assertThat(response.message()).isEqualTo("사용 중인 닉네임입니다."); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("checkNicknameAvailable - 앞뒤 공백은 trim한 뒤 조회하고 응답에도 trim된 값을 돌려준다") | ||
| void checkNicknameAvailable_trimsWhitespaceBeforeLookup() { | ||
| given(userRepository.existsByNickname("김뮤즈")).willReturn(false); | ||
|
|
||
| UserResponseDTO.NicknameCheckResponse response = userService.checkNicknameAvailable(" 김뮤즈 "); | ||
|
|
||
| assertThat(response.nickname()).isEqualTo("김뮤즈"); | ||
| verify(userRepository).existsByNickname("김뮤즈"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("checkNicknameAvailable - 닉네임이 null/공백이면 NICKNAME_REQUIRED 예외를 던지고 DB 조회는 하지 않는다") | ||
| void checkNicknameAvailable_blank_throwsNicknameRequiredWithoutDbLookup() { | ||
| assertThatThrownBy(() -> userService.checkNicknameAvailable(" ")) | ||
| .isInstanceOf(GeneralException.class) | ||
| .extracting(e -> ((GeneralException) e).getCode()) | ||
| .isEqualTo(UserErrorStatus.NICKNAME_REQUIRED); | ||
|
|
||
| verify(userRepository, never()).existsByNickname(org.mockito.ArgumentMatchers.any()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("checkNicknameAvailable - 형식(한글/영문/숫자 2~10자)에 안 맞으면 NICKNAME_INVALID_FORMAT 예외를 던지고 DB 조회는 하지 않는다") | ||
| void checkNicknameAvailable_invalidFormat_throwsInvalidFormatWithoutDbLookup() { | ||
| assertThatThrownBy(() -> userService.checkNicknameAvailable("김@뮤즈")) | ||
| .isInstanceOf(GeneralException.class) | ||
| .extracting(e -> ((GeneralException) e).getCode()) | ||
| .isEqualTo(UserErrorStatus.NICKNAME_INVALID_FORMAT); | ||
|
|
||
| verify(userRepository, never()).existsByNickname(org.mockito.ArgumentMatchers.any()); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.