diff --git a/src/main/java/com/moabam/api/application/member/MemberService.java b/src/main/java/com/moabam/api/application/member/MemberService.java index b54bdfc6..ce4ca41a 100644 --- a/src/main/java/com/moabam/api/application/member/MemberService.java +++ b/src/main/java/com/moabam/api/application/member/MemberService.java @@ -20,12 +20,15 @@ import com.moabam.api.dto.member.MemberInfo; import com.moabam.api.dto.member.MemberInfoResponse; import com.moabam.api.dto.member.MemberInfoSearchResponse; +import com.moabam.api.dto.member.ModifyMemberRequest; import com.moabam.global.auth.model.AuthMember; import com.moabam.global.common.util.ClockHolder; import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.exception.ConflictException; import com.moabam.global.error.exception.NotFoundException; +import io.micrometer.common.util.StringUtils; import lombok.RequiredArgsConstructor; @Service @@ -82,6 +85,26 @@ public MemberInfoResponse searchInfo(AuthMember authMember, Long memberId) { return MemberMapper.toMemberInfoResponse(memberInfoSearchResponse, inventories); } + @Transactional + public void modifyInfo(AuthMember authMember, ModifyMemberRequest modifyMemberRequest, String newProfileUri) { + validateNickname(modifyMemberRequest.nickname()); + + Member member = memberSearchRepository.findMember(authMember.id()) + .orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND)); + + member.changeNickName(modifyMemberRequest.nickname()); + member.changeIntro(modifyMemberRequest.intro()); + member.changeProfileUri(newProfileUri); + + memberRepository.save(member); + } + + private void validateNickname(String nickname) { + if (StringUtils.isEmpty(nickname) && memberRepository.existsByNickname(nickname)) { + throw new ConflictException(NICKNAME_CONFLICT); + } + } + private List getDefaultSkin(Long searchId) { List inventories = inventorySearchRepository.findDefaultSkin(searchId); if (inventories.size() != GlobalConstant.DEFAULT_SKIN_SIZE) { diff --git a/src/main/java/com/moabam/api/domain/member/Member.java b/src/main/java/com/moabam/api/domain/member/Member.java index b013d7fc..33f3aab4 100644 --- a/src/main/java/com/moabam/api/domain/member/Member.java +++ b/src/main/java/com/moabam/api/domain/member/Member.java @@ -84,6 +84,7 @@ private Member(Long id, String socialId, Bug bug) { this.id = id; this.socialId = requireNonNull(socialId); this.nickname = createNickName(); + this.intro = ""; this.profileImage = BaseImageUrl.MEMBER_PROFILE_URL; this.bug = requireNonNull(bug); this.role = Role.USER; @@ -124,7 +125,15 @@ public void delete(LocalDateTime now) { } public void changeNickName(String nickname) { - this.nickname = nickname; + this.nickname = requireNonNullElse(nickname, this.nickname); + } + + public void changeIntro(String intro) { + this.intro = requireNonNullElse(intro, this.intro); + } + + public void changeProfileUri(String newProfileUri) { + this.profileImage = requireNonNullElse(newProfileUri, profileImage); } private String createNickName() { diff --git a/src/main/java/com/moabam/api/domain/member/repository/MemberRepository.java b/src/main/java/com/moabam/api/domain/member/repository/MemberRepository.java index bb0d5436..f0cb499b 100644 --- a/src/main/java/com/moabam/api/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/moabam/api/domain/member/repository/MemberRepository.java @@ -9,4 +9,6 @@ public interface MemberRepository extends JpaRepository { Optional findBySocialId(String id); + + boolean existsByNickname(String nickname); } diff --git a/src/main/java/com/moabam/api/dto/member/ModifyMemberRequest.java b/src/main/java/com/moabam/api/dto/member/ModifyMemberRequest.java new file mode 100644 index 00000000..43c9ef31 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/member/ModifyMemberRequest.java @@ -0,0 +1,8 @@ +package com.moabam.api.dto.member; + +public record ModifyMemberRequest( + String intro, + String nickname +) { + +} diff --git a/src/main/java/com/moabam/api/presentation/MemberController.java b/src/main/java/com/moabam/api/presentation/MemberController.java index 5c5da9a0..4891f5a3 100644 --- a/src/main/java/com/moabam/api/presentation/MemberController.java +++ b/src/main/java/com/moabam/api/presentation/MemberController.java @@ -1,5 +1,7 @@ package com.moabam.api.presentation; +import java.util.List; + import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -7,18 +9,24 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import com.moabam.api.application.auth.AuthorizationService; +import com.moabam.api.application.image.ImageService; import com.moabam.api.application.member.MemberService; +import com.moabam.api.domain.image.ImageType; import com.moabam.api.dto.auth.AuthorizationCodeResponse; import com.moabam.api.dto.auth.AuthorizationTokenInfoResponse; import com.moabam.api.dto.auth.AuthorizationTokenResponse; import com.moabam.api.dto.auth.LoginResponse; import com.moabam.api.dto.member.MemberInfoResponse; +import com.moabam.api.dto.member.ModifyMemberRequest; import com.moabam.global.auth.annotation.Auth; import com.moabam.global.auth.model.AuthMember; +import com.moabam.global.error.exception.BadRequestException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -31,6 +39,7 @@ public class MemberController { private final AuthorizationService authorizationService; private final MemberService memberService; + private final ImageService imageService; @GetMapping("/login/oauth") public void socialLogin(HttpServletResponse httpServletResponse) { @@ -65,4 +74,19 @@ public void deleteMember(@Auth AuthMember authMember) { public MemberInfoResponse searchInfo(@Auth AuthMember authMember, @PathVariable(required = false) Long memberId) { return memberService.searchInfo(authMember, memberId); } + + @PostMapping("/modify") + public void modifyMember(@Auth AuthMember authMember, + @RequestPart(required = false) ModifyMemberRequest modifyMemberRequest, + @RequestPart(name = "profileImage", required = false) MultipartFile newProfileImage) { + String newProfileUri = null; + + try { + newProfileUri = imageService.uploadImages(List.of(newProfileImage), ImageType.PROFILE_IMAGE).get(0); + } catch (BadRequestException | NullPointerException e) { + // Do nothing + } + + memberService.modifyInfo(authMember, modifyMemberRequest, newProfileUri); + } } diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index a03e5962..de5c5a99 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -35,6 +35,7 @@ public enum ErrorMessage { MEMBER_NOT_FOUND_BY_MANAGER_OR_NULL("방의 매니저거나 회원이 존재하지 않습니다."), MEMBER_ROOM_EXCEED("참여할 수 있는 방의 개수가 모두 찼습니다."), UNLINK_REQUEST_FAIL_ROLLBACK_SUCCESS("카카오 연결 요청 실패로 Rollback하였습니다."), + NICKNAME_CONFLICT("이미 존재하는 닉네임입니다."), INVALID_DEFAULT_SKIN_SIZE("기본 스킨은 2개여야 합니다. 관리자에게 문의하세요"), diff --git a/src/main/resources/static/docs/coupon.html b/src/main/resources/static/docs/coupon.html index 4feedc62..071e2e5a 100644 --- a/src/main/resources/static/docs/coupon.html +++ b/src/main/resources/static/docs/coupon.html @@ -461,7 +461,7 @@

요청

POST /admins/coupons HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Content-Length: 175
+Content-Length: 183
 Host: localhost:8080
 
 {
@@ -571,7 +571,7 @@ 

요청

POST /coupons/search HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Content-Length: 41
+Content-Length: 44
 Host: localhost:8080
 
 {
@@ -637,7 +637,7 @@ 

응답

Access-Control-Allow-Credentials: true Access-Control-Max-Age: 3600 Content-Type: application/json -Content-Length: 64 +Content-Length: 66 { "message" : "쿠폰 발급 가능 기간이 아닙니다." diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 62f80fd6..cb72d35f 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -616,7 +616,7 @@

diff --git a/src/main/resources/static/docs/notification.html b/src/main/resources/static/docs/notification.html index d56f528e..a06f23b3 100644 --- a/src/main/resources/static/docs/notification.html +++ b/src/main/resources/static/docs/notification.html @@ -513,7 +513,7 @@

응답

diff --git a/src/test/java/com/moabam/api/application/member/MemberServiceTest.java b/src/test/java/com/moabam/api/application/member/MemberServiceTest.java index c1d9025c..a30978cc 100644 --- a/src/test/java/com/moabam/api/application/member/MemberServiceTest.java +++ b/src/test/java/com/moabam/api/application/member/MemberServiceTest.java @@ -26,6 +26,7 @@ import com.moabam.api.dto.auth.AuthorizationTokenInfoResponse; import com.moabam.api.dto.auth.LoginResponse; import com.moabam.api.dto.member.MemberInfoResponse; +import com.moabam.api.dto.member.ModifyMemberRequest; import com.moabam.global.auth.model.AuthMember; import com.moabam.global.common.util.ClockHolder; import com.moabam.global.error.exception.BadRequestException; @@ -37,6 +38,7 @@ import com.moabam.support.fixture.ItemFixture; import com.moabam.support.fixture.MemberFixture; import com.moabam.support.fixture.MemberInfoSearchFixture; +import com.moabam.support.fixture.ModifyImageFixture; @ExtendWith({MockitoExtension.class, FilterProcessExtension.class}) class MemberServiceTest { @@ -224,4 +226,24 @@ void failBy_overSize(@WithMember AuthMember authMember) { .hasMessage(INVALID_DEFAULT_SKIN_SIZE.getMessage()); } } + + @DisplayName("사용자 정보 수정 성공") + @Test + void modify_success_test(@WithMember AuthMember authMember) { + // given + Member member = MemberFixture.member(); + ModifyMemberRequest modifyMemberRequest = ModifyImageFixture.modifyMemberRequest(); + given(memberSearchRepository.findMember(authMember.id())).willReturn(Optional.ofNullable(member)); + + // when + memberService.modifyInfo(authMember, modifyMemberRequest, "/main"); + + // Then + assertAll( + () -> assertThat(member.getNickname()).isEqualTo(modifyMemberRequest.nickname()), + () -> assertThat(member.getIntro()).isEqualTo(modifyMemberRequest.intro()), + () -> assertThat(member.getProfileImage()).isEqualTo("/main") + ); + } + } diff --git a/src/test/java/com/moabam/api/presentation/MemberControllerTest.java b/src/test/java/com/moabam/api/presentation/MemberControllerTest.java index 8fc82662..73e43428 100644 --- a/src/test/java/com/moabam/api/presentation/MemberControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/MemberControllerTest.java @@ -3,12 +3,14 @@ import static com.moabam.global.common.util.GlobalConstant.*; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -19,15 +21,18 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureMockRestServiceServer; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.match.MockRestRequestMatchers; @@ -39,7 +44,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.moabam.api.application.auth.OAuth2AuthorizationServerRequestService; +import com.moabam.api.application.image.ImageService; import com.moabam.api.domain.auth.repository.TokenRepository; +import com.moabam.api.domain.image.ImageType; import com.moabam.api.domain.item.Inventory; import com.moabam.api.domain.item.Item; import com.moabam.api.domain.item.repository.InventoryRepository; @@ -55,6 +62,7 @@ import com.moabam.api.domain.room.repository.ParticipantRepository; import com.moabam.api.domain.room.repository.RoomRepository; import com.moabam.api.dto.auth.TokenSaveValue; +import com.moabam.api.dto.member.ModifyMemberRequest; import com.moabam.global.config.OAuthConfig; import com.moabam.global.error.exception.UnauthorizedException; import com.moabam.global.error.handler.RestTemplateResponseHandler; @@ -111,6 +119,9 @@ class MemberControllerTest extends WithoutFilterSupporter { @Autowired OAuthConfig oAuthConfig; + @SpyBean + ImageService imageService; + RestTemplateBuilder restTemplateBuilder; MockRestServiceServer mockRestServiceServer; @@ -417,4 +428,65 @@ void search_member_failBy_default_skin_size() throws Exception { .andExpect(status().is4xxClientError()); } + + @DisplayName("회원 정보 요청 성공") + @WithMember + @ParameterizedTest + @CsvSource({"intro,", ", nickname", ",", "intro, nickname"}) + void member_modify_request_success(String intro, String nickname) throws Exception { + // given + ModifyMemberRequest request = new ModifyMemberRequest(intro, nickname); + MockMultipartFile newProfileImage = + new MockMultipartFile( + "profileImage", + "tooth.png", + "multipart/form-data", + "uploadFile".getBytes(StandardCharsets.UTF_8)); + MockMultipartFile modifyMemberRequest = + new MockMultipartFile( + "modifyMemberRequest", + null, + "application/json", + objectMapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8)); + + willReturn(List.of("/main")) + .given(imageService).uploadImages(List.of(newProfileImage), ImageType.PROFILE_IMAGE); + + // expected + mockMvc.perform(multipart(HttpMethod.POST, "/members/modify") + .file(modifyMemberRequest) + .file(newProfileImage) + .contentType("multipart/form-data") + + .characterEncoding("UTF-8")) + .andExpect(status().is2xxSuccessful()) + .andDo(print()); + } + + @DisplayName("회원 프로필없이 성공 ") + @WithMember + @ParameterizedTest + @CsvSource({"intro,", ", nickname", ",", "intro, nickname"}) + void member_modify_no_image_request_success(String intro, String nickname) throws Exception { + // given + ModifyMemberRequest request = new ModifyMemberRequest(intro, nickname); + MockMultipartFile modifyMemberRequest = + new MockMultipartFile( + "modifyMemberRequest", + null, + "application/json", + objectMapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8)); + + willThrow(NullPointerException.class) + .given(imageService).uploadImages(any(), any()); + + // expected + mockMvc.perform(multipart(HttpMethod.POST, "/members/modify") + .file(modifyMemberRequest) + .contentType("multipart/form-data") + + .characterEncoding("UTF-8")) + .andExpect(status().is2xxSuccessful()) + .andDo(print()); + } } diff --git a/src/test/java/com/moabam/support/fixture/ModifyImageFixture.java b/src/test/java/com/moabam/support/fixture/ModifyImageFixture.java new file mode 100644 index 00000000..afee62fd --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/ModifyImageFixture.java @@ -0,0 +1,28 @@ +package com.moabam.support.fixture; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import org.springframework.mock.web.MockMultipartFile; + +import com.moabam.api.dto.member.ModifyMemberRequest; + +public class ModifyImageFixture { + + public static MockMultipartFile makeMultipartFile() { + try { + File file = new File("src/test/resources/image.png"); + FileInputStream fileInputStream = new FileInputStream(file); + + return new MockMultipartFile("1", "image.png", "image/png", fileInputStream); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public static ModifyMemberRequest modifyMemberRequest() { + return new ModifyMemberRequest("intro", "sldsldsld"); + } + +}