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
8 changes: 6 additions & 2 deletions src/docs/asciidoc/users.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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` 내 정보 수정 (미구현)

1 change: 1 addition & 0 deletions src/main/java/com/chooz/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("토큰이 만료됐습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ public ResponseEntity<PresignedUrlResponse> createPresignedUrl(@Valid @RequestBo
PresignedUrlResponse response = imageService.getPresignedUrl(request);
return ResponseEntity.ok(response);
}
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/chooz/user/application/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/com/chooz/user/domain/User.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +15,7 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.util.StringUtils;

import java.util.Optional;

Expand Down Expand Up @@ -46,6 +49,7 @@ private User(
boolean notification,
OnboardingStep onboardingStep
) {
validateNickname(nickname);
this.id = id;
this.nickname = nickname;
this.profileUrl = profileUrl;
Expand All @@ -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();
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/chooz/user/presentation/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -35,6 +37,15 @@ public ResponseEntity<UserMyInfoResponse> findMyInfo(
return ResponseEntity.ok(userService.findByMe(userInfo.userId()));
}

@PutMapping("/me")
public ResponseEntity<Void> updateMyInfo(
@AuthenticationPrincipal UserInfo userInfo,
@Valid @RequestBody UpdateUserRequest updateUserRequest
) {
userService.updateUser(userInfo.userId(), updateUserRequest);
return ResponseEntity.ok().build();
}

@PatchMapping("/onboarding")
public ResponseEntity<UserInfoResponse> findUserInfo(
@Valid @RequestBody OnboardingRequest request,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.chooz.user.presentation.dto;

import jakarta.validation.constraints.NotBlank;

public record UpdateUserRequest(
@NotBlank
String nickname,

String imageUrl
) {}

26 changes: 26 additions & 0 deletions src/test/java/com/chooz/user/application/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +74,7 @@ void createUser_duplicateNickname() {
() -> assertThat(user2.getNickname()).isEqualTo("호기심 많은 츄1")
);
}

@Test
@DisplayName("유저생성 닉넥임 사용가능한 가장 작은 suffix 선택 테스트")
void createUser_minSuffix() {
Expand All @@ -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() {
Expand All @@ -115,6 +140,7 @@ void user_complete_onboarding_step() {
() -> assertThat(onboardingStep.isFirstVote()).isFalse()
);
}

@Test
@DisplayName("온보딩 요청 예외 테스트")
void user_complete_onboarding_step_exception() {
Expand Down
51 changes: 48 additions & 3 deletions src/test/java/com/chooz/user/presentation/UserControllerTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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("유저 아이디")
),
Expand Down Expand Up @@ -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("유저 아이디")
Expand All @@ -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("온보딩 수행")
Expand Down Expand Up @@ -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("온보딩 단계")
Expand Down