From ca7bbfe870e3e739ea1ca9b42a199b4e4e0eda34 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Wed, 24 Jan 2024 03:42:17 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=B5=9C?= =?UTF-8?q?=EB=8C=80=20=EA=B8=B8=EC=9D=B4=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EB=8C=80=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../offonoff/ab/application/service/common/LengthInfo.java | 7 +++---- src/main/java/life/offonoff/ab/domain/comment/Comment.java | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java b/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java index 808cea87..66015688 100644 --- a/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java +++ b/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java @@ -6,10 +6,9 @@ @Getter @RequiredArgsConstructor public enum LengthInfo { - // TODO: 댓글 최대 길이 요구사항대로 수정 - COMMENT_CONTENT(1, 100), - - PAGEABLE_SIZE(0, 100) + COMMENT_CONTENT(1, 255), + PAGEABLE_SIZE(0, 100), + NICKNAME_LENGTH(1, 8) ; private final int minLength; diff --git a/src/main/java/life/offonoff/ab/domain/comment/Comment.java b/src/main/java/life/offonoff/ab/domain/comment/Comment.java index adf124fc..d0654f9b 100644 --- a/src/main/java/life/offonoff/ab/domain/comment/Comment.java +++ b/src/main/java/life/offonoff/ab/domain/comment/Comment.java @@ -18,6 +18,7 @@ public class Comment extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(length = 255) private String content; @ManyToOne(fetch = FetchType.LAZY) From 62d63c974f59e3a72855fa61222ccfeaeb2b0093 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Wed, 24 Jan 2024 03:42:44 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=ED=95=9C=EA=B8=80,=20=EC=98=81?= =?UTF-8?q?=EB=AC=B8,=20=EC=88=AB=EC=9E=90=EB=A7=8C=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=EB=90=90=EB=8A=94=EC=A7=80=20=EC=97=AC=EB=B6=80=20=ED=8C=90?= =?UTF-8?q?=EB=8B=A8=ED=95=98=EB=8A=94=20util=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ab/application/service/common/TextUtils.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/life/offonoff/ab/application/service/common/TextUtils.java b/src/main/java/life/offonoff/ab/application/service/common/TextUtils.java index 54215af2..4cb8952c 100644 --- a/src/main/java/life/offonoff/ab/application/service/common/TextUtils.java +++ b/src/main/java/life/offonoff/ab/application/service/common/TextUtils.java @@ -6,6 +6,9 @@ public class TextUtils { private static final Pattern graphemePattern = Pattern.compile("\\X"); + // 한글, 영문, 숫자 + private static final Pattern specialCharacterFreePattern = Pattern.compile("^[0-9a-zA-Zㄱ-ㅎ가-힣]*$"); + /* * 이모티콘이 포함된 문자와 같이 2byte가 넘는 문자가 있을 경우 String의 길이는 우리가 인식하는 글자 단위보다 길어진다. * 이 함수는 우리가 인식하는 대로 길이를 읽어온다. @@ -36,4 +39,12 @@ public static int countEmojis(String text) { public static int countGraphemeClustersWithLongerEmoji(String text) { return countGraphemeClusters(text) + countEmojis(text); } + + /* + * 한글, 영문, 숫자 + */ + public static boolean isOnlyKoreanEnglishNumberIncluded(String text) { + return specialCharacterFreePattern.matcher(text).matches(); + } + } From 7cafbb3a1900f49ee1751e1e792de9eeaecfa3da Mon Sep 17 00:00:00 2001 From: melonturtle Date: Wed, 24 Jan 2024 03:43:27 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=EC=9D=98=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=EA=B3=BC=20=EC=A7=81=EC=97=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../life/offonoff/ab/domain/member/Member.java | 14 ++++++++++++++ .../offonoff/ab/domain/member/PersonalInfo.java | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/life/offonoff/ab/domain/member/Member.java b/src/main/java/life/offonoff/ab/domain/member/Member.java index a9cf162a..f8f8acdc 100644 --- a/src/main/java/life/offonoff/ab/domain/member/Member.java +++ b/src/main/java/life/offonoff/ab/domain/member/Member.java @@ -252,4 +252,18 @@ public ChoiceOption getVotedOptionOfTopic(Topic topic) { .findAny() .orElse(null); } + + public void updateNickname(String nickname) { + if (personalInfo.getNickname().equals(nickname)) { + return; + } + this.personalInfo.updateNickname(nickname); + } + + public void updateJob(String job) { + if (personalInfo.getJob().equals(job)) { + return; + } + this.personalInfo.updateJob(job); + } } \ No newline at end of file diff --git a/src/main/java/life/offonoff/ab/domain/member/PersonalInfo.java b/src/main/java/life/offonoff/ab/domain/member/PersonalInfo.java index 688601bc..3e9ef190 100644 --- a/src/main/java/life/offonoff/ab/domain/member/PersonalInfo.java +++ b/src/main/java/life/offonoff/ab/domain/member/PersonalInfo.java @@ -7,7 +7,6 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import java.time.LocalDate; @@ -29,4 +28,12 @@ public PersonalInfo(String nickname, LocalDate birthDate, Gender gender, String this.gender = gender; this.job = job; } + + public void updateNickname(String nickname) { + this.nickname = nickname; + } + + public void updateJob(String job) { + this.job = job; + } } From 1e1162b64ae36bac9a4f3b67539c1f404134be08 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Wed, 24 Jan 2024 03:44:17 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../life/offonoff/ab/exception/DuplicateNicknameException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/life/offonoff/ab/exception/DuplicateNicknameException.java b/src/main/java/life/offonoff/ab/exception/DuplicateNicknameException.java index 16bb67c1..ef4e1a47 100644 --- a/src/main/java/life/offonoff/ab/exception/DuplicateNicknameException.java +++ b/src/main/java/life/offonoff/ab/exception/DuplicateNicknameException.java @@ -2,7 +2,7 @@ public class DuplicateNicknameException extends DuplicateException { - private static final String MESSAGE = "중복된 닉네임입니다."; + private static final String MESSAGE = "이미 사용중인 닉네임이에요."; private final String nickname; public DuplicateNicknameException(String nickname) { From b1fc399385132c46da4f234beb78501201c130b9 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Wed, 24 Jan 2024 03:45:58 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20=EB=82=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 멤버 회원가입시 닉네임과 직업 유효성 체크 추가한다 TODO: 직업 유효성 요구사항 추가 후 수정 Issue: #105 --- src/docs/asciidoc/index.adoc | 3 +- src/docs/asciidoc/member.adoc | 26 ++++++ .../application/service/auth/AuthService.java | 13 ++- .../service/member/MemberService.java | 33 ++++++- .../request/MemberProfileInfoRequest.java | 7 ++ .../life/offonoff/ab/exception/AbCode.java | 6 +- .../NotKoreanEnglishNumberException.java | 28 ++++++ .../offonoff/ab/web/MemberController.java | 12 ++- .../service/auth/AuthServiceTest.java | 3 +- .../service/member/MemberServiceTest.java | 89 ++++++++++++++++++ .../offonoff/ab/web/MemberControllerTest.java | 93 +++++++++++++++++++ 11 files changed, 302 insertions(+), 11 deletions(-) create mode 100644 src/docs/asciidoc/member.adoc create mode 100644 src/main/java/life/offonoff/ab/application/service/request/MemberProfileInfoRequest.java create mode 100644 src/main/java/life/offonoff/ab/exception/NotKoreanEnglishNumberException.java create mode 100644 src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java create mode 100644 src/test/java/life/offonoff/ab/web/MemberControllerTest.java diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index f69b1437..6390c6da 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -9,4 +9,5 @@ include::topic.adoc[] include::oauth.adoc[] include::auth.adoc[] include::comment.adoc[] -include::image.adoc[] \ No newline at end of file +include::image.adoc[] +include::member.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc new file mode 100644 index 00000000..87eae31f --- /dev/null +++ b/src/docs/asciidoc/member.adoc @@ -0,0 +1,26 @@ + +== 7. 멤버 API +### 7.1. 멤버 마이페이지 정보 수정 + +[source.html] +POST /members/profile/information + +#### OK + +operation::member-controller-test/update-members-profile-information_with-valid-field_success[snippets="http-request,http-response"] + +#### E1. 닉네임에 한글, 영문, 숫자 외에 문자 포함 + +operation::member-controller-test/update-members-profile-information_with-illegal-letter-nickname_exception[snippets="http-request,http-response"] + +#### E2. 닉네임이 8자 초과 + +operation::member-controller-test/update-members-profile-information_with-long-nickname_exception[snippets="http-request,http-response"] + +#### E3. 닉네임 중복 + +operation::member-controller-test/update-members-profile-information_with-duplicate-nickname_exception[snippets="http-request,http-response"] + +#### E4. 직업 + +직업 필드 유효성은 요구사항 정해지면 수정 예정 \ No newline at end of file diff --git a/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java b/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java index b4883632..d8b39c6b 100644 --- a/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java +++ b/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java @@ -6,7 +6,10 @@ import life.offonoff.ab.application.service.request.auth.SignInRequest; import life.offonoff.ab.application.service.request.auth.SignUpRequest; import life.offonoff.ab.domain.member.Member; -import life.offonoff.ab.exception.*; +import life.offonoff.ab.exception.DuplicateEmailException; +import life.offonoff.ab.exception.IllegalJoinStatusException; +import life.offonoff.ab.exception.IllegalPasswordException; +import life.offonoff.ab.exception.MemberByEmailNotFoundException; import life.offonoff.ab.util.password.PasswordEncoder; import life.offonoff.ab.util.token.TokenProvider; import life.offonoff.ab.web.response.auth.join.JoinStatusResponse; @@ -18,7 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static life.offonoff.ab.domain.member.JoinStatus.*; +import static life.offonoff.ab.domain.member.JoinStatus.AUTH_REGISTERED; @Transactional(readOnly = true) @RequiredArgsConstructor @@ -74,10 +77,10 @@ public JoinStatusResponse registerProfile(ProfileRegisterRequest request) { private void beforeRegisterProfile(ProfileRegisterRequest request) { final String nickname = request.getNickname(); + memberService.checkMembersNickname(nickname); - if (memberService.existsByEmail(nickname)) { - throw new DuplicateNicknameException(nickname); - } + final String job = request.getJob(); + memberService.checkMembersJob(job); } @Transactional diff --git a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java index c8b3a6fa..958efbb6 100644 --- a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java +++ b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java @@ -1,5 +1,8 @@ package life.offonoff.ab.application.service.member; +import life.offonoff.ab.application.service.common.LengthInfo; +import life.offonoff.ab.application.service.common.TextUtils; +import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; import life.offonoff.ab.application.service.request.MemberRequest; import life.offonoff.ab.domain.member.Member; import life.offonoff.ab.exception.*; @@ -51,7 +54,33 @@ public boolean existsByEmail(final String email) { return true; } - public boolean existsByNickname(final String nickname) { - return memberRepository.existsByNickname(nickname); + @Transactional + public void updateMembersProfileInformation(final Long memberId, final MemberProfileInfoRequest request) { + checkMembersNickname(request.nickname()); + checkMembersJob(request.job()); + + Member member = findById(memberId); + member.updateNickname(request.nickname()); + member.updateJob(request.job()); + } + + public void checkMembersNickname(String nickname) { + int length = TextUtils.countGraphemeClusters(nickname); + if (length < LengthInfo.NICKNAME_LENGTH.getMinLength() || length > LengthInfo.NICKNAME_LENGTH.getMaxLength()) { + throw new LengthInvalidException("닉네임", LengthInfo.NICKNAME_LENGTH); + } + + if (!TextUtils.isOnlyKoreanEnglishNumberIncluded(nickname)) { + throw new NotKoreanEnglishNumberException(nickname); + } + + if (memberRepository.existsByNickname(nickname)) { + throw new DuplicateNicknameException(nickname); + } } + + public void checkMembersJob(String job) { + // TODO: 요구사항 안나옴 + } + } diff --git a/src/main/java/life/offonoff/ab/application/service/request/MemberProfileInfoRequest.java b/src/main/java/life/offonoff/ab/application/service/request/MemberProfileInfoRequest.java new file mode 100644 index 00000000..306e3ae6 --- /dev/null +++ b/src/main/java/life/offonoff/ab/application/service/request/MemberProfileInfoRequest.java @@ -0,0 +1,7 @@ +package life.offonoff.ab.application.service.request; + +public record MemberProfileInfoRequest( + String nickname, + String job +) { +} diff --git a/src/main/java/life/offonoff/ab/exception/AbCode.java b/src/main/java/life/offonoff/ab/exception/AbCode.java index 532ab633..f1cd8979 100644 --- a/src/main/java/life/offonoff/ab/exception/AbCode.java +++ b/src/main/java/life/offonoff/ab/exception/AbCode.java @@ -54,5 +54,9 @@ public enum AbCode { EXPIRED_TOKEN, INVALID_SIGNATURE_TOKEN, - FUTURE_TIME_REQUEST; + FUTURE_TIME_REQUEST, + + NOT_KOREAN_ENGLISH_NUMBER, + + ; } diff --git a/src/main/java/life/offonoff/ab/exception/NotKoreanEnglishNumberException.java b/src/main/java/life/offonoff/ab/exception/NotKoreanEnglishNumberException.java new file mode 100644 index 00000000..9812b920 --- /dev/null +++ b/src/main/java/life/offonoff/ab/exception/NotKoreanEnglishNumberException.java @@ -0,0 +1,28 @@ +package life.offonoff.ab.exception; + +import org.springframework.http.HttpStatus; + +public class NotKoreanEnglishNumberException extends AbException { + private static final String MESSAGE = "한글, 영문, 숫자만 가능해요."; + private static final AbCode abCode = AbCode.NOT_KOREAN_ENGLISH_NUMBER; + private final String nickname; + public NotKoreanEnglishNumberException(String nickname) { + super(MESSAGE); + this.nickname = nickname; + } + + @Override + public String getHint() { + return "필드["+nickname+"]에 한글, 영문, 숫자가 아닌 문자가 포함되어있습니다."; + } + + @Override + public int getHttpStatusCode() { + return HttpStatus.BAD_REQUEST.value(); + } + + @Override + public AbCode getAbCode() { + return abCode; + } +} diff --git a/src/main/java/life/offonoff/ab/web/MemberController.java b/src/main/java/life/offonoff/ab/web/MemberController.java index b7f4002d..13644d96 100644 --- a/src/main/java/life/offonoff/ab/web/MemberController.java +++ b/src/main/java/life/offonoff/ab/web/MemberController.java @@ -1,18 +1,20 @@ package life.offonoff.ab.web; import life.offonoff.ab.application.service.member.MemberService; +import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; import life.offonoff.ab.web.common.aspect.auth.Authorized; import life.offonoff.ab.web.response.MemberInfoResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RequiredArgsConstructor -@RequestMapping("/member") +@RequestMapping("/members") @RestController public class MemberController { @@ -23,4 +25,12 @@ public ResponseEntity getMemberInfo(@Authorized Long memberI MemberInfoResponse response = MemberInfoResponse.of(memberService.findById(memberId)); return ResponseEntity.ok(response); } + + @PutMapping("/profile/information") + public ResponseEntity updateMembersProfileInformation( + @Authorized final Long memberId, + final MemberProfileInfoRequest request) { + memberService.updateMembersProfileInformation(memberId, request); + return ResponseEntity.ok().build(); + } } diff --git a/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java b/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java index 86271056..b15c43b8 100644 --- a/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java +++ b/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java @@ -129,7 +129,8 @@ void singup_personalInfo_exception_duplicate_nickname() { "job" ); - when(memberService.existsByEmail(anyString())).thenReturn(true); + doThrow(DuplicateNicknameException.class) + .when(memberService).checkMembersNickname(anyString()); // then assertThatThrownBy(() -> authService.registerProfile(registerRequest)) diff --git a/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java b/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java new file mode 100644 index 00000000..205e4fbd --- /dev/null +++ b/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java @@ -0,0 +1,89 @@ +package life.offonoff.ab.application.service.member; + +import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; +import life.offonoff.ab.domain.TestEntityUtil; +import life.offonoff.ab.domain.member.Member; +import life.offonoff.ab.exception.DuplicateNicknameException; +import life.offonoff.ab.exception.LengthInvalidException; +import life.offonoff.ab.exception.NotKoreanEnglishNumberException; +import life.offonoff.ab.repository.member.MemberRepository; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@Transactional +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + + @InjectMocks + MemberService memberService; + + @Mock + MemberRepository memberRepository; + + @Test + void updateMembersProfileInformation_withValidField_success() { + // given + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "바뀔직업"); + Member member = TestEntityUtil.TestMember.builder() + .id(1L) + .nickname("닉네임") + .job("직업") + .build() + .buildMember(); + when(memberRepository.findById(any())).thenReturn(Optional.of(member)); + + // when + Executable code = () -> + memberService.updateMembersProfileInformation(1L, request); + + // then + assertDoesNotThrow(code); + } + + @Test + void updateMembersProfileInformation_withIllegalLetterNickname_exception() { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉!!!", "바뀔직업"); + + ThrowingCallable code = () -> + memberService.updateMembersProfileInformation(1L, request); + + assertThatThrownBy(code) + .isInstanceOf(NotKoreanEnglishNumberException.class); + } + + @Test + void updateMembersProfileInformation_withLongNickname_exception() { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("무려8자넘는닉네임", "바뀔직업"); + + ThrowingCallable code = () -> + memberService.updateMembersProfileInformation(1L, request); + + assertThatThrownBy(code) + .isInstanceOf(LengthInvalidException.class); + } + + @Test + void updateMembersProfileInformation_withDuplicateNickname_exception() { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("닉네임", "바뀔직업"); + when(memberRepository.existsByNickname(any())).thenReturn(true); + + ThrowingCallable code = () -> + memberService.updateMembersProfileInformation(1L, request); + + assertThatThrownBy(code) + .isInstanceOf(DuplicateNicknameException.class); + } +} \ No newline at end of file diff --git a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java new file mode 100644 index 00000000..b4d797bc --- /dev/null +++ b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java @@ -0,0 +1,93 @@ +package life.offonoff.ab.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import life.offonoff.ab.application.service.common.LengthInfo; +import life.offonoff.ab.application.service.member.MemberService; +import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; +import life.offonoff.ab.config.WebConfig; +import life.offonoff.ab.exception.DuplicateNicknameException; +import life.offonoff.ab.exception.LengthInvalidException; +import life.offonoff.ab.exception.NotKoreanEnglishNumberException; +import life.offonoff.ab.restdocs.RestDocsTest; +import life.offonoff.ab.web.common.aspect.auth.AuthorizedArgumentResolver; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = MemberController.class, + excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebConfig.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AuthorizedArgumentResolver.class) + }) +class MemberControllerTest extends RestDocsTest { + + @MockBean + MemberService memberService; + + @Test + void updateMembersProfileInformation_withValidField_success() throws Exception { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "바뀔직업"); + + mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + void updateMembersProfileInformation_withIllegalLetterNickname_exception() throws Exception { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉!!!", "바뀔직업"); + + doThrow(new NotKoreanEnglishNumberException("바뀔닉!!!")) + .when(memberService).updateMembersProfileInformation(any(), any()); + + mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("abCode").value("NOT_KOREAN_ENGLISH_NUMBER")); + } + + @Test + void updateMembersProfileInformation_withLongNickname_exception() throws Exception { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("무려8자넘는닉네임", "바뀔직업"); + + doThrow(new LengthInvalidException("닉네임", LengthInfo.NICKNAME_LENGTH)) + .when(memberService).updateMembersProfileInformation(any(), any()); + + mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("abCode").value("INVALID_LENGTH_OF_FIELD")); + } + + @Test + void updateMembersProfileInformation_withDuplicateNickname_exception() throws Exception { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "바뀔직업"); + + doThrow(new DuplicateNicknameException("바뀔닉네임")) + .when(memberService).updateMembersProfileInformation(any(), any()); + + mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("abCode").value("DUPLICATE_NICKNAME")); + } + + private static class MemberUri { + private static final String BASE = "/members"; + private static final String PROFILE_INFO = BASE + "/profile/information"; + } +} \ No newline at end of file From 25e207adc72d08ef8fb3be5bb68bc615a2a2ed46 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Wed, 24 Jan 2024 11:10:00 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=EC=82=AD=EC=A0=9C=20API=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/member.adoc | 20 +++++++- .../ab/application/service/S3Service.java | 48 ++++++++++++++++++- .../service/member/MemberService.java | 23 +++++++++ .../service/request/ProfileImageRequest.java | 6 +++ .../offonoff/ab/domain/member/Member.java | 4 ++ .../life/offonoff/ab/exception/AbCode.java | 2 +- .../exception/S3InvalidFileUrlException.java | 27 +++++++++++ .../exception/S3InvalidKeyNameException.java | 27 +++++++++++ .../offonoff/ab/web/MemberController.java | 23 +++++++-- .../offonoff/ab/web/MemberControllerTest.java | 19 ++++++++ 10 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 src/main/java/life/offonoff/ab/application/service/request/ProfileImageRequest.java create mode 100644 src/main/java/life/offonoff/ab/exception/S3InvalidFileUrlException.java create mode 100644 src/main/java/life/offonoff/ab/exception/S3InvalidKeyNameException.java diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc index 87eae31f..0c2a0da9 100644 --- a/src/docs/asciidoc/member.adoc +++ b/src/docs/asciidoc/member.adoc @@ -23,4 +23,22 @@ operation::member-controller-test/update-members-profile-information_with-duplic #### E4. 직업 -직업 필드 유효성은 요구사항 정해지면 수정 예정 \ No newline at end of file +직업 필드 유효성은 요구사항 정해지면 수정 예정 + +### 7.2 멤버의 프로필 이미지 변경 + +[source.html] +PUT members/profile/image + +#### OK + +operation::member-controller-test/update-members-profile-image[snippets="http-request,http-response"] + +### 7.3 멤버의 프로필 이미지 삭제 + +[source.html] +DELETE members/profile/image + +#### OK + +operation::member-controller-test/remove-members-profile-image[snippets="http-request,http-response"] \ No newline at end of file diff --git a/src/main/java/life/offonoff/ab/application/service/S3Service.java b/src/main/java/life/offonoff/ab/application/service/S3Service.java index 505ff57a..57fe3fd5 100644 --- a/src/main/java/life/offonoff/ab/application/service/S3Service.java +++ b/src/main/java/life/offonoff/ab/application/service/S3Service.java @@ -1,6 +1,8 @@ package life.offonoff.ab.application.service; import life.offonoff.ab.exception.IllegalImageExtension; +import life.offonoff.ab.exception.S3InvalidFileUrlException; +import life.offonoff.ab.exception.S3InvalidKeyNameException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -8,6 +10,9 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; @@ -28,7 +33,7 @@ public class S3Service { private final String baseDir; private final Pattern fileNamePattern = Pattern.compile("(.*\\.(jpg|jpeg|png|gif|bmp))", Pattern.CASE_INSENSITIVE); - + private final Pattern keynamePattern = Pattern.compile("^https://.+\\.s3\\..+\\.amazonaws.com/(.+)"); public S3Service( @Value("${cloud.aws.credentials.access-key}") String accessKey, @Value("${cloud.aws.credentials.secret-key}") String secretKey, @@ -112,4 +117,45 @@ private URL createSignedUrlForImagePut(String keyName, String contentType) { return presignedRequest.url(); } } + + public void deleteFile(String fileUrl) { + String keyName = getKeyNameFromFileUrl(fileUrl); + deleteObject(keyName); + } + + private String getKeyNameFromFileUrl(String fileUrl) { + Matcher matcher = keynamePattern.matcher(fileUrl); + if (!matcher.matches()) { + log.warn("S3 파일 삭제 요청 URL이 올바르지 않습니다. | URL: {}", fileUrl); + throw new S3InvalidFileUrlException(fileUrl); + } + String keyName = matcher.group(1); + // 띄어쓰기 포함된 파일 이름의 경우 +를 띄어쓰기로 대체해줘야 삭제됨 + return keyName.replace('+', ' '); + } + + private void deleteObject(String keyName) { + try (S3Client s3 = S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(credentialsProvider) + .build()) { + + DeleteObjectRequest request = DeleteObjectRequest.builder() + .bucket(bucket) + .key(keyName) + .build(); + + DeleteObjectResponse response = s3.deleteObject(request); + boolean isSuccessful = response.sdkHttpResponse().isSuccessful(); + if (!isSuccessful) { + // ! 주의할 것 + // keyName이 존재해서 제대로 삭제되든, + // keyName이 틀려서 존재하지 않는 파일이어도 204 No Content 응답이 온다. + log.warn("keyName["+keyName+"] 삭제 실패 {} {}", response.sdkHttpResponse().statusCode(), response.sdkHttpResponse().statusText()); + } + } catch (Exception e) { + log.warn("S3 삭제 요청 실패 [keyName={}]", keyName, e); + throw new S3InvalidKeyNameException(keyName); + } + } } diff --git a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java index 958efbb6..4f371a55 100644 --- a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java +++ b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java @@ -1,5 +1,6 @@ package life.offonoff.ab.application.service.member; +import life.offonoff.ab.application.service.S3Service; import life.offonoff.ab.application.service.common.LengthInfo; import life.offonoff.ab.application.service.common.TextUtils; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; @@ -17,6 +18,7 @@ public class MemberService { private final MemberRepository memberRepository; + private final S3Service s3Service; //== join ==// @Transactional @@ -83,4 +85,25 @@ public void checkMembersJob(String job) { // TODO: 요구사항 안나옴 } + @Transactional + public void updateMembersProfileImage(Long memberId, String imageUrl) { + Member member = findById(memberId); + removeMembersProfileImage(member); + + member.updateProfileImageUrl(imageUrl); + } + + @Transactional + public void removeMembersProfileImage(Long memberId) { + Member member = findById(memberId); + removeMembersProfileImage(member); + } + + private void removeMembersProfileImage(Member member) { + String originalUrl = member.getProfileImageUrl(); + if (originalUrl != null) { + s3Service.deleteFile(originalUrl); + } + } + } diff --git a/src/main/java/life/offonoff/ab/application/service/request/ProfileImageRequest.java b/src/main/java/life/offonoff/ab/application/service/request/ProfileImageRequest.java new file mode 100644 index 00000000..c3eeff43 --- /dev/null +++ b/src/main/java/life/offonoff/ab/application/service/request/ProfileImageRequest.java @@ -0,0 +1,6 @@ +package life.offonoff.ab.application.service.request; + +public record ProfileImageRequest( + String imageUrl +) { +} diff --git a/src/main/java/life/offonoff/ab/domain/member/Member.java b/src/main/java/life/offonoff/ab/domain/member/Member.java index f8f8acdc..1b170c12 100644 --- a/src/main/java/life/offonoff/ab/domain/member/Member.java +++ b/src/main/java/life/offonoff/ab/domain/member/Member.java @@ -266,4 +266,8 @@ public void updateJob(String job) { } this.personalInfo.updateJob(job); } + + public void updateProfileImageUrl(String imageUrl) { + this.profileImageUrl = imageUrl; + } } \ No newline at end of file diff --git a/src/main/java/life/offonoff/ab/exception/AbCode.java b/src/main/java/life/offonoff/ab/exception/AbCode.java index f1cd8979..ba2941cc 100644 --- a/src/main/java/life/offonoff/ab/exception/AbCode.java +++ b/src/main/java/life/offonoff/ab/exception/AbCode.java @@ -58,5 +58,5 @@ public enum AbCode { NOT_KOREAN_ENGLISH_NUMBER, - ; + S3_INVALID_FILE_URL, S3_INVALID_KEY_NAME; } diff --git a/src/main/java/life/offonoff/ab/exception/S3InvalidFileUrlException.java b/src/main/java/life/offonoff/ab/exception/S3InvalidFileUrlException.java new file mode 100644 index 00000000..4d07f8b8 --- /dev/null +++ b/src/main/java/life/offonoff/ab/exception/S3InvalidFileUrlException.java @@ -0,0 +1,27 @@ +package life.offonoff.ab.exception; + +import org.springframework.http.HttpStatus; + +public class S3InvalidFileUrlException extends AbException { + private static final String MESSAGE = "올바르지 않은 파일 url입니다."; + private final String fileUrl; + public S3InvalidFileUrlException(String fileUrl) { + super(MESSAGE); + this.fileUrl = fileUrl; + } + + @Override + public String getHint() { + return "요청한 URL["+fileUrl+"]이 올바르지 않습니다."; + } + + @Override + public int getHttpStatusCode() { + return HttpStatus.BAD_REQUEST.value(); + } + + @Override + public AbCode getAbCode() { + return AbCode.S3_INVALID_FILE_URL; + } +} diff --git a/src/main/java/life/offonoff/ab/exception/S3InvalidKeyNameException.java b/src/main/java/life/offonoff/ab/exception/S3InvalidKeyNameException.java new file mode 100644 index 00000000..0212055e --- /dev/null +++ b/src/main/java/life/offonoff/ab/exception/S3InvalidKeyNameException.java @@ -0,0 +1,27 @@ +package life.offonoff.ab.exception; + +import org.springframework.http.HttpStatus; + +public class S3InvalidKeyNameException extends AbException { + private static final String MESSAGE = "올바르지 않은 keyname입니다."; + private final String keyName; + public S3InvalidKeyNameException(String keyName) { + super(MESSAGE); + this.keyName = keyName; + } + + @Override + public String getHint() { + return "요청한 keyName["+keyName+"]이 올바르지 않습니다."; + } + + @Override + public int getHttpStatusCode() { + return HttpStatus.BAD_REQUEST.value(); + } + + @Override + public AbCode getAbCode() { + return AbCode.S3_INVALID_KEY_NAME; + } +} diff --git a/src/main/java/life/offonoff/ab/web/MemberController.java b/src/main/java/life/offonoff/ab/web/MemberController.java index 13644d96..21c75bad 100644 --- a/src/main/java/life/offonoff/ab/web/MemberController.java +++ b/src/main/java/life/offonoff/ab/web/MemberController.java @@ -2,15 +2,13 @@ import life.offonoff.ab.application.service.member.MemberService; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; +import life.offonoff.ab.application.service.request.ProfileImageRequest; import life.offonoff.ab.web.common.aspect.auth.Authorized; import life.offonoff.ab.web.response.MemberInfoResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Slf4j @RequiredArgsConstructor @@ -33,4 +31,21 @@ public ResponseEntity updateMembersProfileInformation( memberService.updateMembersProfileInformation(memberId, request); return ResponseEntity.ok().build(); } + + @PutMapping("/profile/image") + public ResponseEntity updateMembersProfileImage( + @Authorized final Long memberId, + final ProfileImageRequest request + ) { + memberService.updateMembersProfileImage(memberId, request.imageUrl()); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/profile/image") + public ResponseEntity removeMembersProfileImage( + @Authorized final Long memberId + ) { + memberService.removeMembersProfileImage(memberId); + return ResponseEntity.ok().build(); + } } diff --git a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java index b4d797bc..18ab9f02 100644 --- a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java +++ b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java @@ -4,6 +4,7 @@ import life.offonoff.ab.application.service.common.LengthInfo; import life.offonoff.ab.application.service.member.MemberService; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; +import life.offonoff.ab.application.service.request.ProfileImageRequest; import life.offonoff.ab.config.WebConfig; import life.offonoff.ab.exception.DuplicateNicknameException; import life.offonoff.ab.exception.LengthInvalidException; @@ -20,6 +21,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -86,8 +88,25 @@ void updateMembersProfileInformation_withDuplicateNickname_exception() throws Ex .andExpect(jsonPath("abCode").value("DUPLICATE_NICKNAME")); } + @Test + void updateMembersProfileImage() throws Exception { + ProfileImageRequest request = new ProfileImageRequest("htttps://tetestst/test.png"); + + mvc.perform(put(MemberUri.PROFILE_IMAGE).with(csrf().asHeader()) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + void removeMembersProfileImage() throws Exception { + mvc.perform(delete(MemberUri.PROFILE_IMAGE).with(csrf().asHeader())) + .andExpect(status().isOk()); + } + private static class MemberUri { private static final String BASE = "/members"; private static final String PROFILE_INFO = BASE + "/profile/information"; + private static final String PROFILE_IMAGE = BASE + "/profile/image"; } } \ No newline at end of file From 8bee4064e67d9962a7ecee992386a9c20591eaf2 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Fri, 26 Jan 2024 04:52:55 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EC=88=98=EC=A0=95,=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: #116 --- src/docs/asciidoc/member.adoc | 16 +++++++- .../application/service/auth/AuthService.java | 8 ++-- .../service/member/MemberService.java | 13 ++++++ .../service/request/TermsUpdateRequest.java | 12 ++++++ .../offonoff/ab/web/MemberController.java | 24 ++++++++++- .../ab/web/response/TermsResponse.java | 11 +++++ ...msResponse.java => JoinTermsResponse.java} | 4 +- .../offonoff/ab/web/AuthControllerTest.java | 4 +- .../offonoff/ab/web/MemberControllerTest.java | 40 +++++++++++++++++-- 9 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 src/main/java/life/offonoff/ab/application/service/request/TermsUpdateRequest.java create mode 100644 src/main/java/life/offonoff/ab/web/response/TermsResponse.java rename src/main/java/life/offonoff/ab/web/response/auth/join/{TermsResponse.java => JoinTermsResponse.java} (62%) diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc index 0c2a0da9..975875ca 100644 --- a/src/docs/asciidoc/member.adoc +++ b/src/docs/asciidoc/member.adoc @@ -41,4 +41,18 @@ DELETE members/profile/image #### OK -operation::member-controller-test/remove-members-profile-image[snippets="http-request,http-response"] \ No newline at end of file +operation::member-controller-test/remove-members-profile-image[snippets="http-request,http-response"] + +### 7.4 멤버의 약관 동의 정보 조회 + +[source.html] +GET members/terms + +operation::member-controller-test/get-members-terms-agreement[snippets="http-request,http-response"] + +### 7.5 멤버의 약관 동의 정보 수정 + +[source.html] +PUT members/terms + +operation::member-controller-test/update-members-terms-agreement[snippets="http-request,http-response"] diff --git a/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java b/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java index d8b39c6b..1c00ba97 100644 --- a/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java +++ b/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java @@ -15,7 +15,7 @@ import life.offonoff.ab.web.response.auth.join.JoinStatusResponse; import life.offonoff.ab.web.response.auth.join.ProfileRegisterResponse; import life.offonoff.ab.web.response.auth.join.SignUpResponse; -import life.offonoff.ab.web.response.auth.join.TermsResponse; +import life.offonoff.ab.web.response.auth.join.JoinTermsResponse; import life.offonoff.ab.web.response.auth.login.SignInResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -90,9 +90,9 @@ public JoinStatusResponse registerTerms(TermsRequest request) { member.agreeTerms(request.toTermsEnabled()); Long memberId = member.getId(); - return new TermsResponse(memberId, - member.getJoinStatus(), - tokenProvider.generateToken(memberId)); + return new JoinTermsResponse(memberId, + member.getJoinStatus(), + tokenProvider.generateToken(memberId)); } //== Sign In ==// diff --git a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java index 4f371a55..edeba193 100644 --- a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java +++ b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java @@ -5,9 +5,11 @@ import life.offonoff.ab.application.service.common.TextUtils; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; import life.offonoff.ab.application.service.request.MemberRequest; +import life.offonoff.ab.application.service.request.TermsUpdateRequest; import life.offonoff.ab.domain.member.Member; import life.offonoff.ab.exception.*; import life.offonoff.ab.repository.member.MemberRepository; +import life.offonoff.ab.web.response.TermsResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -106,4 +108,15 @@ private void removeMembersProfileImage(Member member) { } } + @Transactional + public TermsResponse updateMembersTermsAgreement(final Long memberId, final TermsUpdateRequest request) { + Member member = findById(memberId); + member.agreeTerms(request.toTermsEnabled()); + return TermsResponse.from(member.getTermsEnabled()); + } + + public TermsResponse getMembersTermsAgreement(Long memberId) { + Member member = findById(memberId); + return TermsResponse.from(member.getTermsEnabled()); + } } diff --git a/src/main/java/life/offonoff/ab/application/service/request/TermsUpdateRequest.java b/src/main/java/life/offonoff/ab/application/service/request/TermsUpdateRequest.java new file mode 100644 index 00000000..a6585ae6 --- /dev/null +++ b/src/main/java/life/offonoff/ab/application/service/request/TermsUpdateRequest.java @@ -0,0 +1,12 @@ +package life.offonoff.ab.application.service.request; + +import jakarta.validation.constraints.NotNull; +import life.offonoff.ab.domain.member.TermsEnabled; + +public record TermsUpdateRequest( + boolean marketingTermsEnabled +) { + public TermsEnabled toTermsEnabled() { + return new TermsEnabled(marketingTermsEnabled); + } +} diff --git a/src/main/java/life/offonoff/ab/web/MemberController.java b/src/main/java/life/offonoff/ab/web/MemberController.java index 21c75bad..e47f3614 100644 --- a/src/main/java/life/offonoff/ab/web/MemberController.java +++ b/src/main/java/life/offonoff/ab/web/MemberController.java @@ -3,12 +3,18 @@ import life.offonoff.ab.application.service.member.MemberService; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; import life.offonoff.ab.application.service.request.ProfileImageRequest; +import life.offonoff.ab.application.service.request.TermsUpdateRequest; import life.offonoff.ab.web.common.aspect.auth.Authorized; import life.offonoff.ab.web.response.MemberInfoResponse; +import life.offonoff.ab.web.response.TermsResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @Slf4j @RequiredArgsConstructor @@ -27,7 +33,7 @@ public ResponseEntity getMemberInfo(@Authorized Long memberI @PutMapping("/profile/information") public ResponseEntity updateMembersProfileInformation( @Authorized final Long memberId, - final MemberProfileInfoRequest request) { + @RequestBody final MemberProfileInfoRequest request) { memberService.updateMembersProfileInformation(memberId, request); return ResponseEntity.ok().build(); } @@ -35,7 +41,7 @@ public ResponseEntity updateMembersProfileInformation( @PutMapping("/profile/image") public ResponseEntity updateMembersProfileImage( @Authorized final Long memberId, - final ProfileImageRequest request + @RequestBody final ProfileImageRequest request ) { memberService.updateMembersProfileImage(memberId, request.imageUrl()); return ResponseEntity.ok().build(); @@ -48,4 +54,18 @@ public ResponseEntity removeMembersProfileImage( memberService.removeMembersProfileImage(memberId); return ResponseEntity.ok().build(); } + + @GetMapping("/terms") + public ResponseEntity getMembersTermsAgreement(@Authorized Long memberId) { + return ResponseEntity.ok(memberService.getMembersTermsAgreement(memberId)); + } + + @PutMapping("/terms") + public ResponseEntity updateMembersTermsAgreement( + @Authorized Long memberId, + @RequestBody final TermsUpdateRequest request + ) { + TermsResponse response = memberService.updateMembersTermsAgreement(memberId, request); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/life/offonoff/ab/web/response/TermsResponse.java b/src/main/java/life/offonoff/ab/web/response/TermsResponse.java new file mode 100644 index 00000000..5c30011e --- /dev/null +++ b/src/main/java/life/offonoff/ab/web/response/TermsResponse.java @@ -0,0 +1,11 @@ +package life.offonoff.ab.web.response; + +import life.offonoff.ab.domain.member.TermsEnabled; + +public record TermsResponse( + boolean marketingTermsEnabled +) { + public static TermsResponse from(TermsEnabled termsEnabled) { + return new TermsResponse(termsEnabled.getListenMarketing()); + } +} diff --git a/src/main/java/life/offonoff/ab/web/response/auth/join/TermsResponse.java b/src/main/java/life/offonoff/ab/web/response/auth/join/JoinTermsResponse.java similarity index 62% rename from src/main/java/life/offonoff/ab/web/response/auth/join/TermsResponse.java rename to src/main/java/life/offonoff/ab/web/response/auth/join/JoinTermsResponse.java index f171db94..e2a39b01 100644 --- a/src/main/java/life/offonoff/ab/web/response/auth/join/TermsResponse.java +++ b/src/main/java/life/offonoff/ab/web/response/auth/join/JoinTermsResponse.java @@ -4,11 +4,11 @@ import lombok.Getter; @Getter -public class TermsResponse extends JoinStatusResponse { +public class JoinTermsResponse extends JoinStatusResponse { private String accessToken; - public TermsResponse(Long memberId, JoinStatus status, String accessToken) { + public JoinTermsResponse(Long memberId, JoinStatus status, String accessToken) { super(memberId, status); this.accessToken = accessToken; } diff --git a/src/test/java/life/offonoff/ab/web/AuthControllerTest.java b/src/test/java/life/offonoff/ab/web/AuthControllerTest.java index 8ac0a76d..4179eb26 100644 --- a/src/test/java/life/offonoff/ab/web/AuthControllerTest.java +++ b/src/test/java/life/offonoff/ab/web/AuthControllerTest.java @@ -15,7 +15,7 @@ import life.offonoff.ab.web.common.aspect.auth.AuthorizedArgumentResolver; import life.offonoff.ab.web.response.auth.join.ProfileRegisterResponse; import life.offonoff.ab.web.response.auth.join.SignUpResponse; -import life.offonoff.ab.web.response.auth.join.TermsResponse; +import life.offonoff.ab.web.response.auth.join.JoinTermsResponse; import life.offonoff.ab.web.response.auth.login.SignInResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -151,7 +151,7 @@ void enable_terms() throws Exception { Long memberId = 1L; TermsRequest request = new TermsRequest(memberId, true); - TermsResponse response = new TermsResponse(1L, JoinStatus.COMPLETE, "access_token"); + JoinTermsResponse response = new JoinTermsResponse(1L, JoinStatus.COMPLETE, "access_token"); when(authService.registerTerms(any(TermsRequest.class))).thenReturn(response); diff --git a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java index 18ab9f02..2fb9bd45 100644 --- a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java +++ b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java @@ -5,12 +5,14 @@ import life.offonoff.ab.application.service.member.MemberService; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; import life.offonoff.ab.application.service.request.ProfileImageRequest; +import life.offonoff.ab.application.service.request.TermsUpdateRequest; import life.offonoff.ab.config.WebConfig; import life.offonoff.ab.exception.DuplicateNicknameException; import life.offonoff.ab.exception.LengthInvalidException; import life.offonoff.ab.exception.NotKoreanEnglishNumberException; import life.offonoff.ab.restdocs.RestDocsTest; import life.offonoff.ab.web.common.aspect.auth.AuthorizedArgumentResolver; +import life.offonoff.ab.web.response.TermsResponse; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -20,9 +22,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -41,6 +43,7 @@ void updateMembersProfileInformation_withValidField_success() throws Exception { MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "바뀔직업"); mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andExpect(status().isOk()); @@ -54,6 +57,7 @@ void updateMembersProfileInformation_withIllegalLetterNickname_exception() throw .when(memberService).updateMembersProfileInformation(any(), any()); mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -68,6 +72,7 @@ void updateMembersProfileInformation_withLongNickname_exception() throws Excepti .when(memberService).updateMembersProfileInformation(any(), any()); mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -82,6 +87,7 @@ void updateMembersProfileInformation_withDuplicateNickname_exception() throws Ex .when(memberService).updateMembersProfileInformation(any(), any()); mvc.perform(put(MemberUri.PROFILE_INFO).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -93,6 +99,7 @@ void updateMembersProfileImage() throws Exception { ProfileImageRequest request = new ProfileImageRequest("htttps://tetestst/test.png"); mvc.perform(put(MemberUri.PROFILE_IMAGE).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andExpect(status().isOk()); @@ -100,13 +107,40 @@ void updateMembersProfileImage() throws Exception { @Test void removeMembersProfileImage() throws Exception { - mvc.perform(delete(MemberUri.PROFILE_IMAGE).with(csrf().asHeader())) + mvc.perform(delete(MemberUri.PROFILE_IMAGE).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN")) .andExpect(status().isOk()); } + @Test + void getMembersTermsAgreement() throws Exception { + when(memberService.getMembersTermsAgreement(any())) + .thenReturn(new TermsResponse(true)); + + mvc.perform(get(MemberUri.TERMS) + .header("Authorization", "Bearer ACCESS_TOKEN")) + .andExpect(status().isOk()) + .andExpect(jsonPath("marketingTermsEnabled").value(true)); + } + + @Test + void updateMembersTermsAgreement() throws Exception { + TermsUpdateRequest request = new TermsUpdateRequest(false); + when(memberService.updateMembersTermsAgreement(any(), any())) + .thenReturn(new TermsResponse(false)); + + mvc.perform(put(MemberUri.TERMS).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("marketingTermsEnabled").value(false)); + } + private static class MemberUri { private static final String BASE = "/members"; private static final String PROFILE_INFO = BASE + "/profile/information"; private static final String PROFILE_IMAGE = BASE + "/profile/image"; + private static final String TERMS = BASE + "/terms"; } } \ No newline at end of file From 865451e71c3061e92b1c33c4fabd73c93514bcd9 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Fri, 26 Jan 2024 05:07:54 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EC=A7=81=EC=97=85=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20validation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/member.adoc | 10 ++++-- .../service/common/LengthInfo.java | 3 +- .../service/member/MemberService.java | 9 ++++- .../service/member/MemberServiceTest.java | 33 +++++++++++++++---- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc index 975875ca..06553e9e 100644 --- a/src/docs/asciidoc/member.adoc +++ b/src/docs/asciidoc/member.adoc @@ -9,7 +9,7 @@ POST /members/profile/information operation::member-controller-test/update-members-profile-information_with-valid-field_success[snippets="http-request,http-response"] -#### E1. 닉네임에 한글, 영문, 숫자 외에 문자 포함 +#### E1. 닉네임에 한글, 영문, 숫자 외 문자 포함 operation::member-controller-test/update-members-profile-information_with-illegal-letter-nickname_exception[snippets="http-request,http-response"] @@ -21,9 +21,13 @@ operation::member-controller-test/update-members-profile-information_with-long-n operation::member-controller-test/update-members-profile-information_with-duplicate-nickname_exception[snippets="http-request,http-response"] -#### E4. 직업 +#### E4. 직업에 한글, 영문, 숫자 외 문자 포함 -직업 필드 유효성은 요구사항 정해지면 수정 예정 +닉네임일때 에러와 동일 + +### E5. 직업이 12자 초과 + +닉네임일때 에러와 동일 ### 7.2 멤버의 프로필 이미지 변경 diff --git a/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java b/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java index 66015688..b2883063 100644 --- a/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java +++ b/src/main/java/life/offonoff/ab/application/service/common/LengthInfo.java @@ -8,7 +8,8 @@ public enum LengthInfo { COMMENT_CONTENT(1, 255), PAGEABLE_SIZE(0, 100), - NICKNAME_LENGTH(1, 8) + NICKNAME_LENGTH(1, 8), + JOB_LENGTH(1, 12) ; private final int minLength; diff --git a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java index edeba193..cbd846cd 100644 --- a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java +++ b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java @@ -84,7 +84,14 @@ public void checkMembersNickname(String nickname) { } public void checkMembersJob(String job) { - // TODO: 요구사항 안나옴 + int length = TextUtils.countGraphemeClusters(job); + if (length < LengthInfo.JOB_LENGTH.getMinLength() || length > LengthInfo.JOB_LENGTH.getMaxLength()) { + throw new LengthInvalidException("직업", LengthInfo.JOB_LENGTH); + } + + if (!TextUtils.isOnlyKoreanEnglishNumberIncluded(job)) { + throw new NotKoreanEnglishNumberException(job); + } } @Transactional diff --git a/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java b/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java index 205e4fbd..797970ce 100644 --- a/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java +++ b/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java @@ -10,7 +10,6 @@ import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.function.Executable; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -18,8 +17,8 @@ import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -34,7 +33,7 @@ class MemberServiceTest { MemberRepository memberRepository; @Test - void updateMembersProfileInformation_withValidField_success() { + void updateMembersProfileInformation_withValidFields_success() { // given MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "바뀔직업"); Member member = TestEntityUtil.TestMember.builder() @@ -46,11 +45,11 @@ void updateMembersProfileInformation_withValidField_success() { when(memberRepository.findById(any())).thenReturn(Optional.of(member)); // when - Executable code = () -> - memberService.updateMembersProfileInformation(1L, request); + memberService.updateMembersProfileInformation(1L, request); // then - assertDoesNotThrow(code); + assertThat(member.getPersonalInfo().getJob()).isEqualTo("바뀔직업"); + assertThat(member.getPersonalInfo().getNickname()).isEqualTo("바뀔닉네임"); } @Test @@ -86,4 +85,26 @@ void updateMembersProfileInformation_withDuplicateNickname_exception() { assertThatThrownBy(code) .isInstanceOf(DuplicateNicknameException.class); } + + @Test + void updateMembersProfileInformation_withIllegalLetterJob_exception() { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "바뀔직업!"); + + ThrowingCallable code = () -> + memberService.updateMembersProfileInformation(1L, request); + + assertThatThrownBy(code) + .isInstanceOf(NotKoreanEnglishNumberException.class); + } + + @Test + void updateMembersProfileInformation_withLongJob_exception() { + MemberProfileInfoRequest request = new MemberProfileInfoRequest("바뀔닉네임", "무려12자가넘는직업이라니"); + + ThrowingCallable code = () -> + memberService.updateMembersProfileInformation(1L, request); + + assertThatThrownBy(code) + .isInstanceOf(LengthInvalidException.class); + } } \ No newline at end of file From 2ef7db498ee97670c5b256ae0b9370167b950d29 Mon Sep 17 00:00:00 2001 From: melonturtle Date: Fri, 26 Jan 2024 06:04:24 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20API=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 로그인 시에 탈퇴한 멤버면 exception Issue: #112 --- src/docs/asciidoc/member.adoc | 15 ++++++ src/docs/asciidoc/oauth.adoc | 16 +++++-- .../application/service/auth/AuthService.java | 4 +- .../service/member/MemberService.java | 33 +++++++++++-- .../service/request/MemberStatusRequest.java | 6 +++ .../life/offonoff/ab/exception/AbCode.java | 2 +- .../exception/MemberDeactivatedException.java | 39 ++++++++++++++++ .../offonoff/ab/web/MemberController.java | 14 ++++-- .../service/auth/AuthServiceTest.java | 19 +++++++- .../service/member/MemberServiceTest.java | 37 +++++++++++++++ .../offonoff/ab/web/MemberControllerTest.java | 13 ++++++ .../offonoff/ab/web/OAuthControllerTest.java | 46 ++++++++++++++++--- 12 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 src/main/java/life/offonoff/ab/application/service/request/MemberStatusRequest.java create mode 100644 src/main/java/life/offonoff/ab/exception/MemberDeactivatedException.java diff --git a/src/docs/asciidoc/member.adoc b/src/docs/asciidoc/member.adoc index 06553e9e..ec09c33e 100644 --- a/src/docs/asciidoc/member.adoc +++ b/src/docs/asciidoc/member.adoc @@ -52,6 +52,8 @@ operation::member-controller-test/remove-members-profile-image[snippets="http-re [source.html] GET members/terms +#### OK + operation::member-controller-test/get-members-terms-agreement[snippets="http-request,http-response"] ### 7.5 멤버의 약관 동의 정보 수정 @@ -59,4 +61,17 @@ operation::member-controller-test/get-members-terms-agreement[snippets="http-req [source.html] PUT members/terms +#### OK + operation::member-controller-test/update-members-terms-agreement[snippets="http-request,http-response"] + +### 7.6 멤버 탈퇴 + +아직 복구는 고려하지 않음 + +[source.html] +PUT members/status + +#### OK + +operation::member-controller-test/update-members-status[snippets="http-request,http-response"] \ No newline at end of file diff --git a/src/docs/asciidoc/oauth.adoc b/src/docs/asciidoc/oauth.adoc index 3b1fe24d..436c60bf 100644 --- a/src/docs/asciidoc/oauth.adoc +++ b/src/docs/asciidoc/oauth.adoc @@ -11,20 +11,28 @@ ### 2.1. authorize_code & redirect_uri -OK. 신규 회원 +#### OK. 신규 회원 operation::o-auth-controller-test/oauth_kakao_new_member_by_code[snippets="request-fields,response-fields,http-request,http-response"] -OK. 기존 회원 +#### OK. 기존 회원 operation::o-auth-controller-test/oauth_kakao_existing_member_by_code[snippets="http-request,http-response"] +#### E1. 탈퇴한 회원 + +operation::o-auth-controller-test/oauth_kakao_deactivated_member_by_code[snippets="http-request,http-response"] + ### 2.2. id_token -OK. 신규 회원 +#### OK. 신규 회원 operation::o-auth-controller-test/oauth_kakao_new_member_by_id-token[snippets="http-request,http-response"] -OK. 기존 회원 +#### OK. 기존 회원 operation::o-auth-controller-test/oauth_kakao_existing_member_by_id-token[snippets="http-request,http-response"] + +#### E1. 탈퇴한 회원 + +operation::o-auth-controller-test/oauth_kakao_deactivated_member_by_id-token[snippets="http-request,http-response"] diff --git a/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java b/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java index 1c00ba97..e9e8c47c 100644 --- a/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java +++ b/src/main/java/life/offonoff/ab/application/service/auth/AuthService.java @@ -100,7 +100,7 @@ public SignInResponse signIn(SignInRequest request) { beforeSignIn(request); - Member member = memberService.findByEmail(request.getEmail()); + Member member = memberService.findMember(request.getEmail()); return new SignInResponse(member.getId(), member.getJoinStatus(), @@ -109,7 +109,7 @@ public SignInResponse signIn(SignInRequest request) { private void beforeSignIn(SignInRequest request) { String email = request.getEmail(); - Member member = memberService.findByEmail(email); + Member member = memberService.findMember(email); // email existence if (!memberService.existsByEmail(email)) { diff --git a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java index cbd846cd..a25523fc 100644 --- a/src/main/java/life/offonoff/ab/application/service/member/MemberService.java +++ b/src/main/java/life/offonoff/ab/application/service/member/MemberService.java @@ -39,6 +39,23 @@ public Member findByEmail(final String email) { .orElseThrow(() -> new MemberByEmailNotFoundException(email)); } + public Member findMember(final Long memberId) { + Member member = findById(memberId); + if (!member.isActive()) { + throw new MemberDeactivatedException(memberId); + } + return member; + } + + public Member findMember(final String email) { + Member member = findByEmail(email); + if (!member.isActive()) { + throw new MemberDeactivatedException(email); + } + return member; + } + + //== exists ==// public boolean existsById(final Long memberId) { try { @@ -63,7 +80,7 @@ public void updateMembersProfileInformation(final Long memberId, final MemberPro checkMembersNickname(request.nickname()); checkMembersJob(request.job()); - Member member = findById(memberId); + Member member = findMember(memberId); member.updateNickname(request.nickname()); member.updateJob(request.job()); } @@ -96,7 +113,7 @@ public void checkMembersJob(String job) { @Transactional public void updateMembersProfileImage(Long memberId, String imageUrl) { - Member member = findById(memberId); + Member member = findMember(memberId); removeMembersProfileImage(member); member.updateProfileImageUrl(imageUrl); @@ -104,7 +121,7 @@ public void updateMembersProfileImage(Long memberId, String imageUrl) { @Transactional public void removeMembersProfileImage(Long memberId) { - Member member = findById(memberId); + Member member = findMember(memberId); removeMembersProfileImage(member); } @@ -117,13 +134,19 @@ private void removeMembersProfileImage(Member member) { @Transactional public TermsResponse updateMembersTermsAgreement(final Long memberId, final TermsUpdateRequest request) { - Member member = findById(memberId); + Member member = findMember(memberId); member.agreeTerms(request.toTermsEnabled()); return TermsResponse.from(member.getTermsEnabled()); } public TermsResponse getMembersTermsAgreement(Long memberId) { - Member member = findById(memberId); + Member member = findMember(memberId); return TermsResponse.from(member.getTermsEnabled()); } + + @Transactional + public void activateMember(final Long memberId, final boolean activated) { + Member member = findMember(memberId); + member.activate(activated); + } } diff --git a/src/main/java/life/offonoff/ab/application/service/request/MemberStatusRequest.java b/src/main/java/life/offonoff/ab/application/service/request/MemberStatusRequest.java new file mode 100644 index 00000000..49343001 --- /dev/null +++ b/src/main/java/life/offonoff/ab/application/service/request/MemberStatusRequest.java @@ -0,0 +1,6 @@ +package life.offonoff.ab.application.service.request; + +public record MemberStatusRequest( + boolean activated +) { +} diff --git a/src/main/java/life/offonoff/ab/exception/AbCode.java b/src/main/java/life/offonoff/ab/exception/AbCode.java index ba2941cc..e99a8293 100644 --- a/src/main/java/life/offonoff/ab/exception/AbCode.java +++ b/src/main/java/life/offonoff/ab/exception/AbCode.java @@ -58,5 +58,5 @@ public enum AbCode { NOT_KOREAN_ENGLISH_NUMBER, - S3_INVALID_FILE_URL, S3_INVALID_KEY_NAME; + S3_INVALID_FILE_URL, S3_INVALID_KEY_NAME, DEACTIVATED_MEMBER; } diff --git a/src/main/java/life/offonoff/ab/exception/MemberDeactivatedException.java b/src/main/java/life/offonoff/ab/exception/MemberDeactivatedException.java new file mode 100644 index 00000000..01f5ef0d --- /dev/null +++ b/src/main/java/life/offonoff/ab/exception/MemberDeactivatedException.java @@ -0,0 +1,39 @@ +package life.offonoff.ab.exception; + +import org.springframework.http.HttpStatus; + +public class MemberDeactivatedException extends AbException{ + private static final String MESSAGE = "탈퇴한 회원입니다."; + private static final AbCode abCode = AbCode.DEACTIVATED_MEMBER; + private final Long memberId; + private final String email; + + public MemberDeactivatedException(String email) { + this(null, email); + } + + public MemberDeactivatedException(Long memberId) { + this(memberId, null); + } + + public MemberDeactivatedException(Long memberId, String email) { + super(MESSAGE); + this.memberId = memberId; + this.email = email; + } + + @Override + public String getHint() { + return "탈퇴한 멤버[id="+memberId+", email="+email+"]입니다."; + } + + @Override + public int getHttpStatusCode() { + return HttpStatus.BAD_REQUEST.value(); + } + + @Override + public AbCode getAbCode() { + return abCode; + } +} diff --git a/src/main/java/life/offonoff/ab/web/MemberController.java b/src/main/java/life/offonoff/ab/web/MemberController.java index e47f3614..01166fe5 100644 --- a/src/main/java/life/offonoff/ab/web/MemberController.java +++ b/src/main/java/life/offonoff/ab/web/MemberController.java @@ -2,6 +2,7 @@ import life.offonoff.ab.application.service.member.MemberService; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; +import life.offonoff.ab.application.service.request.MemberStatusRequest; import life.offonoff.ab.application.service.request.ProfileImageRequest; import life.offonoff.ab.application.service.request.TermsUpdateRequest; import life.offonoff.ab.web.common.aspect.auth.Authorized; @@ -11,10 +12,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; @Slf4j @RequiredArgsConstructor @@ -68,4 +65,13 @@ public ResponseEntity updateMembersTermsAgreement( TermsResponse response = memberService.updateMembersTermsAgreement(memberId, request); return ResponseEntity.ok(response); } + + @PutMapping("/status") + public ResponseEntity updateMembersStatus( + @Authorized Long memberId, + @RequestBody final MemberStatusRequest request + ) { + memberService.activateMember(memberId, request.activated()); + return ResponseEntity.ok().build(); + } } diff --git a/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java b/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java index b15c43b8..45a5e0a9 100644 --- a/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java +++ b/src/test/java/life/offonoff/ab/application/service/auth/AuthServiceTest.java @@ -7,6 +7,7 @@ import life.offonoff.ab.domain.member.*; import life.offonoff.ab.exception.DuplicateException; import life.offonoff.ab.exception.DuplicateNicknameException; +import life.offonoff.ab.exception.MemberByEmailNotFoundException; import life.offonoff.ab.exception.MemberNotFoundException; import life.offonoff.ab.util.token.JwtProvider; import life.offonoff.ab.util.password.PasswordEncoder; @@ -48,7 +49,7 @@ void sign_in_test() { when(memberService.existsByEmail(anyString())).thenReturn(true); when(passwordEncoder.isMatch(anyString(), anyString())).thenReturn(true); - when(memberService.findByEmail(anyString())).thenReturn(member); + when(memberService.findMember(anyString())).thenReturn(member); when(jwtProvider.generateToken(nullable(Long.class))).thenReturn("access_token"); // when @@ -73,6 +74,22 @@ void sign_in_exception_invalid_email() { assertThatThrownBy(() -> authService.signIn(request)).isInstanceOf(MemberNotFoundException.class); } + @Test + void signIn_deactivatedMember_exception() { + // given + String email = "email"; + String password = "password"; + + SignInRequest request = new SignInRequest(email, password); + + when(memberService.findMember(anyString())) + .thenThrow(MemberByEmailNotFoundException.class); + + // when + assertThatThrownBy(() -> authService.signIn(request)) + .isInstanceOf(MemberByEmailNotFoundException.class); + } + @Test @DisplayName("정상 회원 가입") void sign_up_test() { diff --git a/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java b/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java index 797970ce..049bb50d 100644 --- a/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java +++ b/src/test/java/life/offonoff/ab/application/service/member/MemberServiceTest.java @@ -5,6 +5,7 @@ import life.offonoff.ab.domain.member.Member; import life.offonoff.ab.exception.DuplicateNicknameException; import life.offonoff.ab.exception.LengthInvalidException; +import life.offonoff.ab.exception.MemberDeactivatedException; import life.offonoff.ab.exception.NotKoreanEnglishNumberException; import life.offonoff.ab.repository.member.MemberRepository; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; @@ -107,4 +108,40 @@ void updateMembersProfileInformation_withLongJob_exception() { assertThatThrownBy(code) .isInstanceOf(LengthInvalidException.class); } + + @Test + void activateMember_deactivate() { + // given + Member member = TestEntityUtil.TestMember.builder() + .id(1L) + .nickname("닉네임") + .job("직업") + .build() + .buildMember(); + when(memberRepository.findById(any())).thenReturn(Optional.of(member)); + + // when + memberService.activateMember(1L, false); + + // then + assertThat(member.isActive()).isFalse(); + } + + @Test + void findMember_deactivatedMember_exception() { + Member member = TestEntityUtil.TestMember.builder() + .id(1L) + .nickname("닉네임") + .job("직업") + .build() + .buildMember(); + member.activate(false); + when(memberRepository.findById(any())).thenReturn(Optional.of(member)); + + ThrowingCallable code = () -> + memberService.findMember(1L); + + assertThatThrownBy(code) + .isInstanceOf(MemberDeactivatedException.class); + } } \ No newline at end of file diff --git a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java index 2fb9bd45..08afe614 100644 --- a/src/test/java/life/offonoff/ab/web/MemberControllerTest.java +++ b/src/test/java/life/offonoff/ab/web/MemberControllerTest.java @@ -4,6 +4,7 @@ import life.offonoff.ab.application.service.common.LengthInfo; import life.offonoff.ab.application.service.member.MemberService; import life.offonoff.ab.application.service.request.MemberProfileInfoRequest; +import life.offonoff.ab.application.service.request.MemberStatusRequest; import life.offonoff.ab.application.service.request.ProfileImageRequest; import life.offonoff.ab.application.service.request.TermsUpdateRequest; import life.offonoff.ab.config.WebConfig; @@ -137,10 +138,22 @@ void updateMembersTermsAgreement() throws Exception { .andExpect(jsonPath("marketingTermsEnabled").value(false)); } + @Test + void updateMembersStatus() throws Exception { + MemberStatusRequest request = new MemberStatusRequest(false); + + mvc.perform(put(MemberUri.STATUS).with(csrf().asHeader()) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()); + } + private static class MemberUri { private static final String BASE = "/members"; private static final String PROFILE_INFO = BASE + "/profile/information"; private static final String PROFILE_IMAGE = BASE + "/profile/image"; private static final String TERMS = BASE + "/terms"; + private static final String STATUS = BASE + "/status"; } } \ No newline at end of file diff --git a/src/test/java/life/offonoff/ab/web/OAuthControllerTest.java b/src/test/java/life/offonoff/ab/web/OAuthControllerTest.java index 54d96ee9..100ec12c 100644 --- a/src/test/java/life/offonoff/ab/web/OAuthControllerTest.java +++ b/src/test/java/life/offonoff/ab/web/OAuthControllerTest.java @@ -5,6 +5,7 @@ import life.offonoff.ab.application.service.request.oauth.OAuthRequest; import life.offonoff.ab.config.WebConfig; import life.offonoff.ab.domain.member.JoinStatus; +import life.offonoff.ab.exception.MemberDeactivatedException; import life.offonoff.ab.restdocs.RestDocsTest; import life.offonoff.ab.util.token.JwtProvider; import life.offonoff.ab.web.common.aspect.auth.AuthorizedArgumentResolver; @@ -17,13 +18,16 @@ import org.springframework.context.annotation.FilterType; import org.springframework.http.MediaType; -import static life.offonoff.ab.application.service.request.oauth.AuthorizeType.*; -import static life.offonoff.ab.domain.member.JoinStatus.*; -import static org.mockito.Mockito.*; +import static life.offonoff.ab.application.service.request.oauth.AuthorizeType.BY_CODE; +import static life.offonoff.ab.application.service.request.oauth.AuthorizeType.BY_IDTOKEN; +import static life.offonoff.ab.domain.member.JoinStatus.AUTH_REGISTERED; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(value = OAuthController.class, excludeFilters = { @@ -86,6 +90,21 @@ void oauth_kakao_existing_member_by_code() throws Exception { .andDo(print()); } + @Test + void oauth_kakao_deactivated_member_by_code() throws Exception { + OAuthRequest request = new OAuthRequest(BY_CODE, "authorize_code", "redirect_uri", null); + + when(oAuthService.authorize(any())) + .thenThrow(MemberDeactivatedException.class); + + mvc.perform(post(OAuthUri.BASE + OAuthUri.KAKAO + OAuthUri.AUTHORIZE) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.abCode").value("DEACTIVATED_MEMBER")) + .andDo(print()); + } + @Test void oauth_kakao_new_member_by_idToken() throws Exception { // given @@ -118,6 +137,21 @@ void oauth_kakao_existing_member_by_idToken() throws Exception { .andDo(print()); } + @Test + void oauth_kakao_deactivated_member_by_idToken() throws Exception { + OAuthRequest request = new OAuthRequest(BY_IDTOKEN, null, null, "id_token"); + + when(oAuthService.authorize(any())) + .thenThrow(MemberDeactivatedException.class); + + mvc.perform(post(OAuthUri.BASE + OAuthUri.KAKAO + OAuthUri.AUTHORIZE) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.abCode").value("DEACTIVATED_MEMBER")) + .andDo(print()); + } + private static class OAuthUri { private static final String BASE = "/oauth"; private static final String KAKAO = "/kakao";