From a7aecbc6742dc8b24d306e401a885dee8128102c Mon Sep 17 00:00:00 2001 From: llddang <77055208+llddang@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:00:12 +0900 Subject: [PATCH] =?UTF-8?q?Feature/#238=20=EA=B5=90=EC=A7=81=EC=9B=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 비밀번호 변경 api의 위치를 command service 레이어로 이동 #238 * feat: 회원의 기본정보(이름, 전화번호)수정 API 구현 및 테스트 코드 구현 #238 * refactor: 변경 도메인이 상단에 존재하도록 request 파일 명 변경 #238 --- backend/src/docs/asciidoc/index.adoc | 14 +++++++- .../sw_css/member/api/MemberController.java | 16 +++++++-- .../application/MemberCommandService.java | 36 +++++++++++++++++++ .../application/MemberQueryService.java | 11 ------ .../dto/request/ChangePasswordRequest.java | 15 -------- .../dto/request/MemberChangeInfoRequest.java | 12 +++++++ .../request/MemberChangePasswordRequest.java | 16 +++++++++ .../java/sw_css/member/domain/Member.java | 2 ++ .../member/exception/MemberExceptionType.java | 2 +- .../java/sw_css/restdocs/RestDocsTest.java | 4 +++ .../restdocs/docs/MemberApiDocsTest.java | 33 +++++++++++++++-- 11 files changed, 127 insertions(+), 34 deletions(-) create mode 100644 backend/src/main/java/sw_css/member/application/MemberCommandService.java delete mode 100644 backend/src/main/java/sw_css/member/application/dto/request/ChangePasswordRequest.java create mode 100644 backend/src/main/java/sw_css/member/application/dto/request/MemberChangeInfoRequest.java create mode 100644 backend/src/main/java/sw_css/member/application/dto/request/MemberChangePasswordRequest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 1024c972..d1459ce5 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -54,7 +54,7 @@ include::{snippets}/faculty-find-all/http-response.adoc[] include::{snippets}/faculty-find-all/response-fields.adoc[] -=== `PATCH`: 비밀번호 수정 +=== `PATCH`: 자신의 비밀번호 수정 .HTTP Request include::{snippets}/member-change-password/http-request.adoc[] @@ -65,6 +65,18 @@ include::{snippets}/member-change-password/request-fields.adoc[] .HTTP Response include::{snippets}/member-change-password/http-response.adoc[] + +=== `PATCH`: 자신의 기본 정보 수정 + +.HTTP Request +include::{snippets}/member-change-info/http-request.adoc[] + +.Request Body +include::{snippets}/member-change-info/request-fields.adoc[] + +.HTTP Response +include::{snippets}/member-change-info/http-response.adoc[] + == 학과 === `GET`: 단과대학 목록 조회 diff --git a/backend/src/main/java/sw_css/member/api/MemberController.java b/backend/src/main/java/sw_css/member/api/MemberController.java index f7e5ec0a..41c34c94 100644 --- a/backend/src/main/java/sw_css/member/api/MemberController.java +++ b/backend/src/main/java/sw_css/member/api/MemberController.java @@ -11,8 +11,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import sw_css.member.application.MemberCommandService; import sw_css.member.application.MemberQueryService; -import sw_css.member.application.dto.request.ChangePasswordRequest; +import sw_css.member.application.dto.request.MemberChangePasswordRequest; +import sw_css.member.application.dto.request.MemberChangeInfoRequest; import sw_css.member.application.dto.response.StudentMemberResponse; import sw_css.member.domain.Member; import sw_css.utils.annotation.MemberInterface; @@ -24,6 +26,7 @@ @Transactional public class MemberController { private final MemberQueryService memberQueryService; + private final MemberCommandService memberCommandService; @GetMapping("/{memberId}") public ResponseEntity findStudent(@PathVariable final Long memberId) { @@ -32,8 +35,15 @@ public ResponseEntity findStudent(@PathVariable final Lon @PatchMapping("/change-password") public ResponseEntity changeMemberPassword(@MemberInterface Member me, - @RequestBody @Valid ChangePasswordRequest request) { - memberQueryService.changePassword(me, request.oldPassword(), request.newPassword()); + @RequestBody @Valid MemberChangePasswordRequest request) { + memberCommandService.changePassword(me, request.oldPassword(), request.newPassword()); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/change-info") + public ResponseEntity changeMemberInfo(@MemberInterface Member me, + @RequestBody @Valid MemberChangeInfoRequest request){ + memberCommandService.changeDefaultInfo(me, request.name(), request.phoneNumber()); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/sw_css/member/application/MemberCommandService.java b/backend/src/main/java/sw_css/member/application/MemberCommandService.java new file mode 100644 index 00000000..7de906d5 --- /dev/null +++ b/backend/src/main/java/sw_css/member/application/MemberCommandService.java @@ -0,0 +1,36 @@ +package sw_css.member.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.member.domain.Member; +import sw_css.member.domain.embedded.Password; +import sw_css.member.domain.repository.MemberRepository; +import sw_css.member.exception.MemberException; +import sw_css.member.exception.MemberExceptionType; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberCommandService { + + private final MemberRepository memberRepository; + + public void changePassword(Member me, String oldPassword, String newPassword) { + if (me.isWrongPassword(oldPassword)) { + throw new MemberException(MemberExceptionType.MEMBER_WRONG_PASSWORD); + } + + String encodedPassword = Password.encode(newPassword); + me.setPassword(encodedPassword); + + memberRepository.save(me); + } + + public void changeDefaultInfo(final Member me, final String name, final String phoneNumber) { + me.setName(name); + me.setPhoneNumber(phoneNumber); + + memberRepository.save(me); + } +} diff --git a/backend/src/main/java/sw_css/member/application/MemberQueryService.java b/backend/src/main/java/sw_css/member/application/MemberQueryService.java index e9bc7d56..4fddb02b 100644 --- a/backend/src/main/java/sw_css/member/application/MemberQueryService.java +++ b/backend/src/main/java/sw_css/member/application/MemberQueryService.java @@ -24,16 +24,5 @@ public StudentMemberResponse findStudentMember(final Long memberId) { .orElseThrow(() -> new MemberException(MemberExceptionType.NOT_FOUND_STUDENT)); return StudentMemberResponse.from(student); } - - public void changePassword(Member me, String oldPassword, String newPassword) { - if (me.isWrongPassword(oldPassword)) { - throw new MemberException(MemberExceptionType.MEMBER_WRONG_PASSWORD); - } - - String encodedPassword = Password.encode(newPassword); - me.setPassword(encodedPassword); - - memberRepository.save(me); - } } diff --git a/backend/src/main/java/sw_css/member/application/dto/request/ChangePasswordRequest.java b/backend/src/main/java/sw_css/member/application/dto/request/ChangePasswordRequest.java deleted file mode 100644 index e319ddfc..00000000 --- a/backend/src/main/java/sw_css/member/application/dto/request/ChangePasswordRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package sw_css.member.application.dto.request; - -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; - -public record ChangePasswordRequest( - @NotNull(message = "이전 비밀번호 값을 입력해주세요.") - String oldPassword, - @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*\\W)[A-Za-z\\d\\W]{6,15}$", message = "비밀번호는 6자에서 15자 이내의 영문, 숫자, 특수문자의 조합이어야 합니다.") - String newPassword) { - - public static ChangePasswordRequest from(String oldPassword, String newPassword) { - return new ChangePasswordRequest(oldPassword, newPassword); - } -} diff --git a/backend/src/main/java/sw_css/member/application/dto/request/MemberChangeInfoRequest.java b/backend/src/main/java/sw_css/member/application/dto/request/MemberChangeInfoRequest.java new file mode 100644 index 00000000..c1bad676 --- /dev/null +++ b/backend/src/main/java/sw_css/member/application/dto/request/MemberChangeInfoRequest.java @@ -0,0 +1,12 @@ +package sw_css.member.application.dto.request; + +import jakarta.validation.constraints.Pattern; +import sw_css.member.domain.embedded.PhoneNumber; +import sw_css.member.domain.embedded.RealName; + +public record MemberChangeInfoRequest( + @Pattern(regexp = RealName.NAME_REGEX, message = RealName.NAME_INVALID) + String name, + @Pattern(regexp = PhoneNumber.PHONE_NUMBER_REGEX, message = PhoneNumber.PHONE_NUMBER_INVALID) + String phoneNumber) { +} diff --git a/backend/src/main/java/sw_css/member/application/dto/request/MemberChangePasswordRequest.java b/backend/src/main/java/sw_css/member/application/dto/request/MemberChangePasswordRequest.java new file mode 100644 index 00000000..3680551d --- /dev/null +++ b/backend/src/main/java/sw_css/member/application/dto/request/MemberChangePasswordRequest.java @@ -0,0 +1,16 @@ +package sw_css.member.application.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import sw_css.member.domain.embedded.Password; + +public record MemberChangePasswordRequest( + @NotBlank(message = "이전 비밀번호 값을 입력해주세요.") + String oldPassword, + @Pattern(regexp = Password.PASSWORD_REGEX, message = Password.PASSWORD_INVALID) + String newPassword) { + + public static MemberChangePasswordRequest from(String oldPassword, String newPassword) { + return new MemberChangePasswordRequest(oldPassword, newPassword); + } +} diff --git a/backend/src/main/java/sw_css/member/domain/Member.java b/backend/src/main/java/sw_css/member/domain/Member.java index e1cc6a1f..2a99ee77 100644 --- a/backend/src/main/java/sw_css/member/domain/Member.java +++ b/backend/src/main/java/sw_css/member/domain/Member.java @@ -27,6 +27,7 @@ public class Member extends BaseEntity { @Column(nullable = false) private String email; + @Setter(AccessLevel.PUBLIC) @Column(nullable = false) private String name; @@ -34,6 +35,7 @@ public class Member extends BaseEntity { @Column(nullable = false) private String password; + @Setter(AccessLevel.PUBLIC) @Column(nullable = false) private String phoneNumber; diff --git a/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java b/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java index 8f25c5a9..bafbd011 100644 --- a/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java +++ b/backend/src/main/java/sw_css/member/exception/MemberExceptionType.java @@ -5,7 +5,7 @@ public enum MemberExceptionType implements BaseExceptionType { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."), - MEMBER_WRONG_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 잘못되었습니다."), + MEMBER_WRONG_PASSWORD(HttpStatus.BAD_REQUEST, "입력한 이전 비밀번호가 일치하지 않습니다."), NOT_FOUND_STUDENT(HttpStatus.NOT_FOUND, "해당하는 학생이 존재하지 않습니다."), INVALID_SEARCH_FIELD_ID(HttpStatus.BAD_REQUEST, "검색 유형이 올바르지 않습니다."), ; diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index b828e236..db5d92ee 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -33,6 +33,7 @@ import sw_css.hackathon.application.HackathonTeamVoteQueryService; import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; +import sw_css.member.application.MemberCommandService; import sw_css.member.application.MemberQueryService; import sw_css.member.domain.repository.MemberRepository; import sw_css.milestone.application.MilestoneHistoryCommandService; @@ -68,6 +69,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected MemberQueryService memberQueryService; + @MockBean + protected MemberCommandService memberCommandService; + @MockBean protected MemberAdminQueryService memberAdminQueryService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java index 2f51e396..0a1c8d83 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java @@ -22,7 +22,8 @@ import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import sw_css.member.api.MemberController; -import sw_css.member.application.dto.request.ChangePasswordRequest; +import sw_css.member.application.dto.request.MemberChangePasswordRequest; +import sw_css.member.application.dto.request.MemberChangeInfoRequest; import sw_css.member.application.dto.response.StudentMemberResponse; import sw_css.member.domain.Member; import sw_css.restdocs.RestDocsTest; @@ -77,10 +78,10 @@ public void changePassword() throws Exception { final String newPassword = "asdf1234!"; final String token = "Bearer AccessToken"; - final ChangePasswordRequest request = new ChangePasswordRequest(oldPassword, newPassword); + final MemberChangePasswordRequest request = new MemberChangePasswordRequest(oldPassword, newPassword); // when - doNothing().when(memberQueryService).changePassword(me, oldPassword, newPassword); + doNothing().when(memberCommandService).changePassword(me, oldPassword, newPassword); // then mockMvc.perform(RestDocumentationRequestBuilders.patch("/members/change-password") @@ -90,4 +91,30 @@ public void changePassword() throws Exception { .andExpect(status().isNoContent()) .andDo(document("member-change-password", requestFields)); } + + @Test + @DisplayName("[성공] 회원은 이름과 전화번호를 변경할 수 있다.") + public void changeDefaultInfo() throws Exception { + // given + final RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("회원의 이름"), + fieldWithPath("phoneNumber").type(JsonFieldType.STRING).description("회원의 전화번호") + ); + + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); + final String token = "Bearer AccessToken"; + + final MemberChangeInfoRequest request = new MemberChangeInfoRequest("이다은", "01031315656"); + + // when + doNothing().when(memberCommandService).changePassword(me, request.name(), request.phoneNumber()); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.patch("/members/change-info") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("member-change-info", requestFields)); + } }