diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc index 5742b2fd..62ecbf07 100644 --- a/src/docs/asciidoc/users.adoc +++ b/src/docs/asciidoc/users.adoc @@ -11,10 +11,14 @@ operation::user-controller-test/find-user-info[snippets='http-request,curl-reque operation::user-controller-test/find-me[snippets='http-request,curl-request,request-headers,http-response,response-fields'] +[[본인-정보-조회]] +=== `PUT` 내 정보 수정 + +operation::user-controller-test/find-me[snippets='http-request,curl-request,request-headers,http-response'] + [[온보딩-단계-완료]] === `PATCH` 온보딩 단계 완료 operation::user-controller-test/complete-step[snippets='http-request,curl-request,request-headers,request-fields,http-response,response-fields'] -[[본인-정보-조회]] -=== `GET` 내 정보 수정 (미구현) + diff --git a/src/main/java/com/chooz/common/exception/ErrorCode.java b/src/main/java/com/chooz/common/exception/ErrorCode.java index 6154c662..868c7e72 100644 --- a/src/main/java/com/chooz/common/exception/ErrorCode.java +++ b/src/main/java/com/chooz/common/exception/ErrorCode.java @@ -47,6 +47,7 @@ public enum ErrorCode { NOT_POST_POLL_CHOICE_ID("게시글의 투표 선택지가 아님"), ONLY_SELF_CAN_CLOSE("작성자 마감의 경우, SELF 마감 방식만이 마감 가능합니다."), INVALID_ONBOARDING_STEP("유효하지 않은 온보딩 단계."), + NICKNAME_LENGTH_EXCEEDED("닉네임 길이 초과"), //401 EXPIRED_TOKEN("토큰이 만료됐습니다."), diff --git a/src/main/java/com/chooz/image/presentation/ImageController.java b/src/main/java/com/chooz/image/presentation/ImageController.java index 32583046..48b8725a 100644 --- a/src/main/java/com/chooz/image/presentation/ImageController.java +++ b/src/main/java/com/chooz/image/presentation/ImageController.java @@ -23,4 +23,4 @@ public ResponseEntity createPresignedUrl(@Valid @RequestBo PresignedUrlResponse response = imageService.getPresignedUrl(request); return ResponseEntity.ok(response); } -} +} \ No newline at end of file diff --git a/src/main/java/com/chooz/user/application/UserService.java b/src/main/java/com/chooz/user/application/UserService.java index e87845d8..8ff57bfc 100644 --- a/src/main/java/com/chooz/user/application/UserService.java +++ b/src/main/java/com/chooz/user/application/UserService.java @@ -6,6 +6,7 @@ import com.chooz.user.domain.OnboardingStepRepository; import com.chooz.user.domain.User; import com.chooz.user.presentation.dto.OnboardingRequest; +import com.chooz.user.presentation.dto.UpdateUserRequest; import com.chooz.user.presentation.dto.UserInfoResponse; import com.chooz.user.presentation.dto.UserMyInfoResponse; import lombok.RequiredArgsConstructor; @@ -35,6 +36,13 @@ private String getOrGenerateNickname(String nickname) { .orElseGet(nicknameGenerator::generate); } + @Transactional + public void updateUser(Long userId, UpdateUserRequest updateUserRequest) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + user.update(updateUserRequest.nickname(), updateUserRequest.imageUrl()); + } + @Transactional(readOnly = true) public UserInfoResponse findById(Long userId) { User user = userRepository.findById(userId) diff --git a/src/main/java/com/chooz/user/domain/User.java b/src/main/java/com/chooz/user/domain/User.java index b1d7cbb7..5b75fae9 100644 --- a/src/main/java/com/chooz/user/domain/User.java +++ b/src/main/java/com/chooz/user/domain/User.java @@ -1,6 +1,8 @@ package com.chooz.user.domain; import com.chooz.common.domain.BaseEntity; +import com.chooz.common.exception.BadRequestException; +import com.chooz.common.exception.ErrorCode; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -13,6 +15,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; import java.util.Optional; @@ -46,6 +49,7 @@ private User( boolean notification, OnboardingStep onboardingStep ) { + validateNickname(nickname); this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; @@ -56,12 +60,21 @@ private User( public static User create(String nickname, String profileUrl) { return new User(null, nickname, getOrDefaultProfileImage(profileUrl), false, new OnboardingStep()); } + public void update(String nickname, String profileUrl) { + validateNickname(nickname); + this.nickname = nickname; + this.profileUrl = getOrDefaultProfileImage(profileUrl); + } + private static void validateNickname(String nickname) { + if(StringUtils.hasText(nickname) && nickname.length() > 15) { + throw new BadRequestException(ErrorCode.NICKNAME_LENGTH_EXCEEDED); + } + } private static String getOrDefaultProfileImage(String profileImageUrl) { return Optional.ofNullable(profileImageUrl) .orElse(User.DEFAULT_PROFILE_URL); } - public boolean hasCompletedOnboarding() { return onboardingStep != null && onboardingStep.isCompletedAll(); } diff --git a/src/main/java/com/chooz/user/presentation/UserController.java b/src/main/java/com/chooz/user/presentation/UserController.java index dbe458a7..957e4604 100644 --- a/src/main/java/com/chooz/user/presentation/UserController.java +++ b/src/main/java/com/chooz/user/presentation/UserController.java @@ -3,6 +3,7 @@ import com.chooz.auth.domain.UserInfo; import com.chooz.user.application.UserService; import com.chooz.user.presentation.dto.OnboardingRequest; +import com.chooz.user.presentation.dto.UpdateUserRequest; import com.chooz.user.presentation.dto.UserInfoResponse; import com.chooz.user.presentation.dto.UserMyInfoResponse; import jakarta.validation.Valid; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -35,6 +37,15 @@ public ResponseEntity findMyInfo( return ResponseEntity.ok(userService.findByMe(userInfo.userId())); } + @PutMapping("/me") + public ResponseEntity updateMyInfo( + @AuthenticationPrincipal UserInfo userInfo, + @Valid @RequestBody UpdateUserRequest updateUserRequest + ) { + userService.updateUser(userInfo.userId(), updateUserRequest); + return ResponseEntity.ok().build(); + } + @PatchMapping("/onboarding") public ResponseEntity findUserInfo( @Valid @RequestBody OnboardingRequest request, diff --git a/src/main/java/com/chooz/user/presentation/dto/UpdateUserRequest.java b/src/main/java/com/chooz/user/presentation/dto/UpdateUserRequest.java new file mode 100644 index 00000000..68e2b835 --- /dev/null +++ b/src/main/java/com/chooz/user/presentation/dto/UpdateUserRequest.java @@ -0,0 +1,11 @@ +package com.chooz.user.presentation.dto; + +import jakarta.validation.constraints.NotBlank; + +public record UpdateUserRequest( + @NotBlank + String nickname, + + String imageUrl +) {} + diff --git a/src/test/java/com/chooz/user/application/UserServiceTest.java b/src/test/java/com/chooz/user/application/UserServiceTest.java index 3ae39673..ad107ede 100644 --- a/src/test/java/com/chooz/user/application/UserServiceTest.java +++ b/src/test/java/com/chooz/user/application/UserServiceTest.java @@ -11,6 +11,7 @@ import com.chooz.user.domain.User; import com.chooz.user.domain.UserRepository; import com.chooz.user.presentation.dto.OnboardingRequest; +import com.chooz.user.presentation.dto.UpdateUserRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -73,6 +74,7 @@ void createUser_duplicateNickname() { () -> assertThat(user2.getNickname()).isEqualTo("호기심 많은 츄1") ); } + @Test @DisplayName("유저생성 닉넥임 사용가능한 가장 작은 suffix 선택 테스트") void createUser_minSuffix() { @@ -94,6 +96,29 @@ void createUser_minSuffix() { ); } + + @Test + @DisplayName("유저 정보 수정 테스트") + void updateUser() { + // given + saveNickNameAdjective("호기심 많은"); + User user = saveUser(); + + // when + UpdateUserRequest updateUserRequest = new UpdateUserRequest( + "이직 하는 츄", + "https://cdn.chooz.site/looking_job_chu.png" + ); + userService.updateUser(user.getId(), updateUserRequest); + + // when then + assertAll( + () -> assertThat(user.getNickname()).isEqualTo("이직 하는 츄"), + () -> assertThat(user.getProfileUrl()).contains("looking_job_chu.png") + ); + } + + @Test @DisplayName("온보딩 수행 테스트") void user_complete_onboarding_step() { @@ -115,6 +140,7 @@ void user_complete_onboarding_step() { () -> assertThat(onboardingStep.isFirstVote()).isFalse() ); } + @Test @DisplayName("온보딩 요청 예외 테스트") void user_complete_onboarding_step_exception() { diff --git a/src/test/java/com/chooz/user/presentation/UserControllerTest.java b/src/test/java/com/chooz/user/presentation/UserControllerTest.java index 6d913283..f1d50679 100644 --- a/src/test/java/com/chooz/user/presentation/UserControllerTest.java +++ b/src/test/java/com/chooz/user/presentation/UserControllerTest.java @@ -1,13 +1,19 @@ package com.chooz.user.presentation; +import com.chooz.post.domain.CloseType; +import com.chooz.post.domain.CommentActive; +import com.chooz.post.domain.PollType; +import com.chooz.post.domain.Scope; import com.chooz.support.RestDocsTest; import com.chooz.support.WithMockUserInfo; import com.chooz.user.domain.OnboardingStepType; import com.chooz.user.presentation.dto.OnboardingRequest; +import com.chooz.user.presentation.dto.UpdateUserRequest; import com.chooz.user.presentation.dto.UserInfoResponse; import com.chooz.user.presentation.dto.UserMyInfoResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; @@ -17,11 +23,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -47,10 +55,12 @@ void findUserInfo() throws Exception { .willReturn(response); System.out.println(objectMapper.writeValueAsString(response)); //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/users/{userId}", "1")) + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/{userId}", "1") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( + requestHeaders(authorizationHeader()), pathParameters( parameterWithName("userId").description("유저 아이디") ), @@ -100,10 +110,12 @@ void findMe() throws Exception { .willReturn(response); //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/users/me")) + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/me") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( + requestHeaders(authorizationHeader()), responseFields( fieldWithPath("id") .description("유저 아이디") @@ -129,6 +141,37 @@ void findMe() throws Exception { ) )); } + + @Test + @WithMockUserInfo + @DisplayName("본인 정보 수정") + void updateMe() throws Exception { + //given + UpdateUserRequest updateUserRequest = new UpdateUserRequest( + "nickname", + "https://cdn.chooz.site/default_profile.png" + ); + + //when then + mockMvc.perform(put("/users/me") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateUserRequest)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + requestFields( + fieldWithPath("nickname") + .type(JsonFieldType.STRING) + .description("닉네임") + .attributes(constraints("1~15자 사이")), + fieldWithPath("imageUrl") + .type(JsonFieldType.STRING) + .description("이미지 경로") + ) + )); + } + @Test @WithMockUserInfo @DisplayName("온보딩 수행") @@ -160,10 +203,12 @@ void completeStep () throws Exception { // when & then mockMvc.perform(RestDocumentationRequestBuilders.patch("/users/onboarding") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( + requestHeaders(authorizationHeader()), requestFields( fieldWithPath("onboardingStep") .description("온보딩 단계")