From 1adc88e76676ba2b2bebdd8dbfc802d0214165b9 Mon Sep 17 00:00:00 2001 From: yunseongoh Date: Tue, 19 Aug 2025 00:49:24 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat=20:=20user=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chooz/common/exception/ErrorCode.java | 1 + .../chooz/image/presentation/ImageController.java | 2 +- .../com/chooz/user/application/UserService.java | 8 ++++++++ src/main/java/com/chooz/user/domain/User.java | 15 ++++++++++++++- .../chooz/user/presentation/UserController.java | 11 +++++++++++ .../user/presentation/dto/UpdateUserRequest.java | 11 +++++++++++ 6 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/chooz/user/presentation/dto/UpdateUserRequest.java 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..64f9e532 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/profile") + 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 +) {} + From 1de57c345cf42b5365b30b27e6100ee5cd196809 Mon Sep 17 00:00:00 2001 From: yunseongoh Date: Tue, 19 Aug 2025 23:59:56 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test=20:=20test=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/UserServiceTest.java | 26 ++++++++++ .../user/presentation/UserControllerTest.java | 51 +++++++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) 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("온보딩 단계") From c0ab8992fe229c48cca308211f9bc301416cf4cd Mon Sep 17 00:00:00 2001 From: yunseongoh Date: Wed, 20 Aug 2025 00:00:30 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix=20:=20endpoint=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/chooz/user/presentation/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/chooz/user/presentation/UserController.java b/src/main/java/com/chooz/user/presentation/UserController.java index 64f9e532..957e4604 100644 --- a/src/main/java/com/chooz/user/presentation/UserController.java +++ b/src/main/java/com/chooz/user/presentation/UserController.java @@ -37,7 +37,7 @@ public ResponseEntity findMyInfo( return ResponseEntity.ok(userService.findByMe(userInfo.userId())); } - @PutMapping("/me/profile") + @PutMapping("/me") public ResponseEntity updateMyInfo( @AuthenticationPrincipal UserInfo userInfo, @Valid @RequestBody UpdateUserRequest updateUserRequest From aa7b167554601f1b974df1b30d8ca76590a09f80 Mon Sep 17 00:00:00 2001 From: yunseongoh Date: Wed, 20 Aug 2025 00:26:19 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs=20:=20=EB=82=B4=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/users.adoc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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` 내 정보 수정 (미구현) +