From 4a11b0c562e07659869c48d0789ee6b8d820d173 Mon Sep 17 00:00:00 2001 From: Kimun Kim Date: Sun, 12 Mar 2023 18:08:30 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20(#917)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 비밀번호 변경 요청 dto 생성 * feat: MemberService.changePassword() 패스워드 변경 기능 추가 * refactor: ChangePasswordRequest dto final 속성 제거 * feat: 비밀번호 변경 API 생성 * refactor: MemberService.changePassword() 파라미터 final 속성 추가 --------- Co-authored-by: kimun kim --- .../controller/MemberController.java | 7 ++ .../woowacourse/zzimkkong/domain/Member.java | 4 + .../dto/member/ChangePasswordRequest.java | 32 ++++++++ ...firmationNewPasswordMismatchException.java | 12 +++ .../member/PasswordMismatchException.java | 12 +++ .../zzimkkong/service/MemberService.java | 30 +++++-- .../controller/MemberControllerTest.java | 41 ++++++++++ .../dto/ChangePasswordRequestTest.java | 44 ++++++++++ .../zzimkkong/service/MemberServiceTest.java | 81 +++++++++++++++++-- 9 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java create mode 100644 backend/src/main/java/com/woowacourse/zzimkkong/exception/member/ConfirmationNewPasswordMismatchException.java create mode 100644 backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java create mode 100644 backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/controller/MemberController.java b/backend/src/main/java/com/woowacourse/zzimkkong/controller/MemberController.java index a9bf6c73d..c4cc872a1 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/controller/MemberController.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/controller/MemberController.java @@ -2,6 +2,7 @@ import com.woowacourse.zzimkkong.config.logaspect.LogMethodExecutionTime; import com.woowacourse.zzimkkong.domain.LoginEmail; +import com.woowacourse.zzimkkong.dto.member.ChangePasswordRequest; import com.woowacourse.zzimkkong.dto.member.*; import com.woowacourse.zzimkkong.dto.member.oauth.OauthMemberSaveRequest; import com.woowacourse.zzimkkong.service.MemberService; @@ -76,4 +77,10 @@ public ResponseEntity getEmojis() { ProfileEmojisResponse profileEmojis = memberService.getProfileEmojis(); return ResponseEntity.ok().body(profileEmojis); } + + @PutMapping("/me/password") + public ResponseEntity changePassword(@LoginEmail final LoginUserEmail loginUserEmail, @RequestBody @Valid final ChangePasswordRequest changePasswordRequest) { + memberService.changePassword(loginUserEmail, changePasswordRequest); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/domain/Member.java b/backend/src/main/java/com/woowacourse/zzimkkong/domain/Member.java index 005e42449..13b4d4e40 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/domain/Member.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/domain/Member.java @@ -78,4 +78,8 @@ public void update(final MemberUpdateRequest memberUpdateRequest) { public boolean hasEmail(String email) { return this.email.equals(email); } + + public void updatePassword(String password) { + this.password = password; + } } diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java new file mode 100644 index 000000000..4e06d4b19 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java @@ -0,0 +1,32 @@ +package com.woowacourse.zzimkkong.dto.member; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import static com.woowacourse.zzimkkong.dto.ValidatorMessage.*; + +@Getter +@NoArgsConstructor +public class ChangePasswordRequest { + @NotNull(message = EMPTY_MESSAGE) + @Pattern(regexp = MEMBER_PW_FORMAT, message = MEMBER_PW_MESSAGE) + private String oldPassword; + + @NotNull(message = EMPTY_MESSAGE) + @Pattern(regexp = MEMBER_PW_FORMAT, message = MEMBER_PW_MESSAGE) + private String newPassword; + + @NotNull(message = EMPTY_MESSAGE) + @Pattern(regexp = MEMBER_PW_FORMAT, message = MEMBER_PW_MESSAGE) + private String newPasswordConfirm; + + public ChangePasswordRequest(String oldPassword, String newPassword, String newPasswordConfirm) { + this.oldPassword = oldPassword; + this.newPassword = newPassword; + this.newPasswordConfirm = newPasswordConfirm; + } +} diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/ConfirmationNewPasswordMismatchException.java b/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/ConfirmationNewPasswordMismatchException.java new file mode 100644 index 000000000..be83663f4 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/ConfirmationNewPasswordMismatchException.java @@ -0,0 +1,12 @@ +package com.woowacourse.zzimkkong.exception.member; + +import com.woowacourse.zzimkkong.exception.ZzimkkongException; +import org.springframework.http.HttpStatus; + +public class ConfirmationNewPasswordMismatchException extends ZzimkkongException { + private static final String MESSAGE = "새 비밀번호가 확인란과 일치하지 않습니다."; + + public ConfirmationNewPasswordMismatchException() { + super(MESSAGE, HttpStatus.BAD_REQUEST); + } +} diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java b/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java new file mode 100644 index 000000000..26036aaff --- /dev/null +++ b/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java @@ -0,0 +1,12 @@ +package com.woowacourse.zzimkkong.exception.member; + +import com.woowacourse.zzimkkong.exception.InputFieldException; +import org.springframework.http.HttpStatus; + +public class PasswordMismatchException extends InputFieldException { + private static final String MESSAGE = "비밀번호가 일치하지 않습니다."; + + public PasswordMismatchException() { + super(MESSAGE, HttpStatus.BAD_REQUEST, PASSWORD); + } +} diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java b/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java index dd6041bd2..5635c357d 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java @@ -5,21 +5,15 @@ import com.woowacourse.zzimkkong.domain.ProfileEmoji; import com.woowacourse.zzimkkong.dto.member.*; import com.woowacourse.zzimkkong.dto.member.oauth.OauthMemberSaveRequest; -import com.woowacourse.zzimkkong.exception.member.DuplicateEmailException; -import com.woowacourse.zzimkkong.exception.member.DuplicateUserNameException; -import com.woowacourse.zzimkkong.exception.member.NoSuchMemberException; -import com.woowacourse.zzimkkong.exception.member.ReservationExistsOnMemberException; +import com.woowacourse.zzimkkong.exception.member.*; import com.woowacourse.zzimkkong.repository.MemberRepository; import com.woowacourse.zzimkkong.repository.ReservationRepository; -import net.logstash.logback.encoder.org.apache.commons.lang3.StringUtils; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; @Service @Transactional @@ -113,4 +107,26 @@ public void deleteMember(final LoginUserEmail loginUserEmail) { public ProfileEmojisResponse getProfileEmojis() { return ProfileEmojisResponse.from(List.of(ProfileEmoji.values())); } + + public void changePassword(final LoginUserEmail loginUserEmail, final ChangePasswordRequest changePasswordRequest) { + Member member = members.findByEmail(loginUserEmail.getEmail()) + .orElseThrow(NoSuchMemberException::new); + validateOldPassword(member, changePasswordRequest); + validateConfirmationPassword(changePasswordRequest); + + String newPassword = passwordEncoder.encode(changePasswordRequest.getNewPassword()); + member.updatePassword(newPassword); + } + + private void validateOldPassword(final Member member, final ChangePasswordRequest changePasswordRequest) { + if (!passwordEncoder.matches(changePasswordRequest.getOldPassword(), member.getPassword())) { + throw new PasswordMismatchException(); + } + } + + private void validateConfirmationPassword(final ChangePasswordRequest changePasswordRequest) { + if (!changePasswordRequest.getNewPassword().equals(changePasswordRequest.getNewPasswordConfirm())) { + throw new ConfirmationNewPasswordMismatchException(); + } + } } diff --git a/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java b/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java index 42bcb9d80..96c34c20f 100644 --- a/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java @@ -153,6 +153,24 @@ void findEmojis() { assertThat(actual).usingRecursiveComparison().isEqualTo(expected); } + @Test + @DisplayName("비밀번호를 변경할 수 있다.") + void changePassword() { + //given + String newPassword = "newPassword1234"; + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(PW, newPassword, newPassword); + + //when + ExtractableResponse response = changePassword(changePasswordRequest); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + + LoginRequest loginRequest = new LoginRequest(EMAIL, newPassword); + ExtractableResponse loginResponse = login(loginRequest); + assertThat(loginResponse.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + static ExtractableResponse saveMember(final MemberSaveRequest memberSaveRequest) { return RestAssured .given(getRequestSpecification()).log().all() @@ -240,4 +258,27 @@ private ExtractableResponse findProfileEmojis() { .when().get("/api/members/emojis") .then().log().all().extract(); } + + private ExtractableResponse changePassword(ChangePasswordRequest changePasswordRequest) { + return RestAssured + .given(getRequestSpecification()).log().all() + .accept("application/json") + .header("Authorization", AuthorizationExtractor.AUTHENTICATION_TYPE + " " + accessToken) + .filter(document("member/password/put", getRequestPreprocessor(), getResponsePreprocessor())) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(changePasswordRequest) + .when().put("/api/members/me/password/put") + .then().log().all().extract(); + } + + private ExtractableResponse login(final LoginRequest loginRequest) { + return RestAssured + .given(getRequestSpecification()).log().all() + .accept("application/json") + .filter(document("member/login", getRequestPreprocessor(), getResponsePreprocessor())) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(loginRequest) + .when().post("/api/members/login/token") + .then().log().all().extract(); + } } diff --git a/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java b/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java new file mode 100644 index 000000000..547a825fb --- /dev/null +++ b/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java @@ -0,0 +1,44 @@ +package com.woowacourse.zzimkkong.dto; + +import com.woowacourse.zzimkkong.dto.member.ChangePasswordRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import static com.woowacourse.zzimkkong.dto.ValidatorMessage.MEMBER_PW_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; + +class ChangePasswordRequestTest extends RequestTest { + @ParameterizedTest + @NullAndEmptySource + @DisplayName("기존 비밀번호 확인란에 빈 문자열이 들어오면 처리한다.") + void blankOldPassword(String oldPassword) { + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(oldPassword, "newPassword", "newPasswordConfirm"); + + assertThat(getConstraintViolations(changePasswordRequest).stream() + .anyMatch(violation -> violation.getMessage().equals(MEMBER_PW_MESSAGE))) + .isTrue(); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("새 비밀번호란에 빈 문자열이 들어오면 처리한다.") + void blankNewPassword(String newPassword) { + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest("oldPassword", newPassword, "newPasswordConfirm"); + + assertThat(getConstraintViolations(changePasswordRequest).stream() + .anyMatch(violation -> violation.getMessage().equals(MEMBER_PW_MESSAGE))) + .isTrue(); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("새 비밀번호 확인란에 빈 문자열이 들어오면 처리한다.") + void blankNewPasswordConfirm(String newPasswordConfirm) { + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest("oldPassword", "newPassword", newPasswordConfirm); + + assertThat(getConstraintViolations(changePasswordRequest).stream() + .anyMatch(violation -> violation.getMessage().equals(MEMBER_PW_MESSAGE))) + .isTrue(); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java b/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java index c965b34fe..c9f9f4da4 100644 --- a/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java +++ b/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java @@ -3,14 +3,9 @@ import com.woowacourse.zzimkkong.domain.Member; import com.woowacourse.zzimkkong.domain.OauthProvider; import com.woowacourse.zzimkkong.domain.ProfileEmoji; -import com.woowacourse.zzimkkong.dto.member.MemberSaveRequest; -import com.woowacourse.zzimkkong.dto.member.MemberSaveResponse; -import com.woowacourse.zzimkkong.dto.member.MemberUpdateRequest; +import com.woowacourse.zzimkkong.dto.member.*; import com.woowacourse.zzimkkong.dto.member.oauth.OauthMemberSaveRequest; -import com.woowacourse.zzimkkong.exception.member.DuplicateEmailException; -import com.woowacourse.zzimkkong.exception.member.DuplicateUserNameException; -import com.woowacourse.zzimkkong.exception.member.ReservationExistsOnMemberException; -import com.woowacourse.zzimkkong.dto.member.LoginUserEmail; +import com.woowacourse.zzimkkong.exception.member.*; import com.woowacourse.zzimkkong.infrastructure.oauth.OauthHandler; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,6 +13,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDateTime; import java.util.Optional; @@ -34,6 +30,9 @@ class MemberServiceTest extends ServiceTest { @Autowired private MemberService memberService; + @Autowired + private PasswordEncoder passwordEncoder; + @MockBean private OauthHandler oauthHandler; @@ -250,4 +249,72 @@ void validateDuplicateUserName() { assertThatThrownBy(() -> memberService.validateDuplicateUserName("중복된이름")) .isInstanceOf(DuplicateUserNameException.class); } + + @Test + @DisplayName("비밀번호를 변경할 수 있다.") + void changePassword() { + // given + LoginUserEmail loginUserEmail = LoginUserEmail.from(EMAIL); + Member member = Member.builder() + .email(EMAIL) + .userName(POBI) + .emoji(ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST) + .password(passwordEncoder.encode(PW)) + .organization(ORGANIZATION) + .build(); + given(members.findByEmail(anyString())) + .willReturn(Optional.of(member)); + + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(PW, "newPassword1234", "newPassword1234"); + + //when + memberService.changePassword(loginUserEmail, changePasswordRequest); + + //then + assertThat(passwordEncoder.matches(changePasswordRequest.getNewPassword(), member.getPassword())).isTrue(); + } + + @Test + @DisplayName("기존 비밀번호를 오기입하면 비밀변호를 변경할 수 없다.") + void changePasswordFailedWhenOldPasswordMismatch() { + // given + LoginUserEmail loginUserEmail = LoginUserEmail.from(EMAIL); + Member member = Member.builder() + .email(EMAIL) + .userName(POBI) + .emoji(ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST) + .password(passwordEncoder.encode(PW)) + .organization(ORGANIZATION) + .build(); + given(members.findByEmail(anyString())) + .willReturn(Optional.of(member)); + + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest("wrongPassword1234", "newPassword1234", "newPassword1234"); + + //when, then + assertThatThrownBy(() -> memberService.changePassword(loginUserEmail, changePasswordRequest)) + .isInstanceOf(PasswordMismatchException.class); + } + + @Test + @DisplayName("새 비밀번호와 새 비밀번호 확인란이 불일치하면 비밀번호를 변경할 수 없다.") + void changePasswordFailedWhenNewPasswordConfirmation() { + // given + LoginUserEmail loginUserEmail = LoginUserEmail.from(EMAIL); + Member member = Member.builder() + .email(EMAIL) + .userName(POBI) + .emoji(ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST) + .password(passwordEncoder.encode(PW)) + .organization(ORGANIZATION) + .build(); + given(members.findByEmail(anyString())) + .willReturn(Optional.of(member)); + + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(PW, "wrongConfirmationPassword1234", "newPassword1234"); + + //when, then + assertThatThrownBy(() -> memberService.changePassword(loginUserEmail, changePasswordRequest)) + .isInstanceOf(ConfirmationNewPasswordMismatchException.class); + } } From 23a520373766d93fe9b824c8c863889f7a5371c7 Mon Sep 17 00:00:00 2001 From: Kimun Kim Date: Sun, 12 Mar 2023 18:39:24 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=97=90=EC=84=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EC=8A=A4=EC=8A=A4=EB=A1=9C=EC=9D=98=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=EC=9D=80=20=EC=A4=91=EB=B3=B5=EA=B2=80?= =?UTF-8?q?=EC=82=AC=EC=97=90=EC=84=9C=20=EC=A0=9C=EC=99=B8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=B6=84=EA=B8=B0=20=EC=83=9D=EC=84=B1=20(#922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: kimun kim --- .../zzimkkong/service/MemberService.java | 4 +- .../zzimkkong/service/MemberServiceTest.java | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java b/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java index 5635c357d..c335d3fc4 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/service/MemberService.java @@ -87,7 +87,9 @@ public void updateMember(final LoginUserEmail loginUserEmail, final MemberUpdate Member member = members.findByEmail(loginUserEmail.getEmail()) .orElseThrow(NoSuchMemberException::new); - validateDuplicateUserName(memberUpdateRequest.getUserName()); + if (!member.getUserName().equals(memberUpdateRequest.getUserName())) { + validateDuplicateUserName(memberUpdateRequest.getUserName()); + } member.update(memberUpdateRequest); } diff --git a/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java b/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java index c9f9f4da4..5de04dfcc 100644 --- a/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java +++ b/backend/src/test/java/com/woowacourse/zzimkkong/service/MemberServiceTest.java @@ -191,6 +191,53 @@ void updateMember() { assertThat(members.findByEmail(EMAIL).orElseThrow().getUserName()).isEqualTo("sakjung"); } + @Test + @DisplayName("회원이 자신의 정보를 수정할 때 중복된 닉네임을 입력하면 실패한다.") + void updateMemberFailWhenDuplicatedUsername() { + // given + LoginUserEmail loginUserEmail = LoginUserEmail.from(EMAIL); + Member member = Member.builder() + .email(EMAIL) + .userName(POBI) + .emoji(ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST) + .password(PW) + .organization(ORGANIZATION) + .build(); + MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest("sakjung", ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST); + + given(members.findByEmail(anyString())) + .willReturn(Optional.of(member)); + given(members.existsByUserName(anyString())) + .willReturn(true); + + // when, then + assertThatThrownBy(() -> memberService.updateMember(loginUserEmail, memberUpdateRequest)) + .isInstanceOf(DuplicateUserNameException.class); + } + + @Test + @DisplayName("회원이 자신의 정보를 수정할 때, 자신의 닉네임은 중복 검사에서 제외한다.") + void updateMemberIgnoreUsernameValidationWhenGivenUsernameAsIs() { + // given + LoginUserEmail loginUserEmail = LoginUserEmail.from(EMAIL); + Member member = Member.builder() + .email(EMAIL) + .userName(POBI) + .emoji(ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST) + .password(PW) + .organization(ORGANIZATION) + .build(); + MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest(member.getUserName(), ProfileEmoji.MAN_DARK_SKIN_TONE_TECHNOLOGIST); + + given(members.findByEmail(anyString())) + .willReturn(Optional.of(member)); + given(members.existsByUserName(anyString())) + .willReturn(true); + + // when, then + assertDoesNotThrow(() -> memberService.updateMember(loginUserEmail, memberUpdateRequest)); + } + @Test @DisplayName("회원을 삭제할 수 있다.") void deleteMember() { From e51544798c6a31d2d3451c83a69465b36ef2d63c Mon Sep 17 00:00:00 2001 From: Jungseok Sung Date: Sun, 12 Mar 2023 18:50:38 +0900 Subject: [PATCH 03/15] =?UTF-8?q?docs:=20build-fe.sh=20script=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EC=84=B8=ED=8C=85=20=EB=B6=80=EB=B6=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#921)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단순 커맨드 검증으로 변경 --- backend/src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index c1db19ec5..ac5532c57 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit c1db19ec5ca3783305af99b6ca2af61b432cbcb2 +Subproject commit ac5532c57e77c5d8ed294aad4986a03d0aaac2dd From b8677b02b61529eea848e36e7d1bfa487b8c0882 Mon Sep 17 00:00:00 2001 From: Jungseok Sung Date: Sun, 12 Mar 2023 18:52:34 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=EB=B9=84=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=A1=B0=ED=9A=8C=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?(#919)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 비회원 예약 조회시, 진행중인 예약도 조회되도록 수정 issue number #911 * feat: 검색 결과 없음 문구 추가 * fix: 불필요한 api 요청 제거 --- .../repository/ReservationRepository.java | 2 +- .../zzimkkong/service/ReservationService.java | 2 +- .../infiniteQuery/useNonLoginReservations.ts | 4 ++++ .../GuestNonLoginReservationSearch.tsx | 18 ++++++++--------- ...tNonLoginReservationSearchResult.styles.ts | 18 +++++++++++++++++ .../GuestNonLoginReservationSearchResult.tsx | 20 ++++++++++++------- 6 files changed, 45 insertions(+), 19 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/repository/ReservationRepository.java b/backend/src/main/java/com/woowacourse/zzimkkong/repository/ReservationRepository.java index fedc331f4..6dec7e574 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/repository/ReservationRepository.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/repository/ReservationRepository.java @@ -42,7 +42,7 @@ Slice findAllByMemberAndReservationTimeDateLessThanEqualAndReservat final LocalDateTime dateTime, final Pageable pageable); - Slice findAllByUserNameAndReservationTimeDateGreaterThanEqualAndReservationTimeStartTimeGreaterThanEqualAndMemberIsNull( + Slice findAllByUserNameAndReservationTimeDateGreaterThanEqualAndReservationTimeEndTimeGreaterThanEqualAndMemberIsNull( final String userName, final LocalDate date, final LocalDateTime dateTime, diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/service/ReservationService.java b/backend/src/main/java/com/woowacourse/zzimkkong/service/ReservationService.java index 15800d34a..6d9bdb11c 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/service/ReservationService.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/service/ReservationService.java @@ -179,7 +179,7 @@ public ReservationInfiniteScrollResponse findUpcomingNonLoginReservations( final String userName, final LocalDateTime searchStartTime, final Pageable pageable) { - Slice reservationSlice = reservations.findAllByUserNameAndReservationTimeDateGreaterThanEqualAndReservationTimeStartTimeGreaterThanEqualAndMemberIsNull( + Slice reservationSlice = reservations.findAllByUserNameAndReservationTimeDateGreaterThanEqualAndReservationTimeEndTimeGreaterThanEqualAndMemberIsNull( userName, TimeZoneUtils.convertTo(searchStartTime, ServiceZone.KOREA).toLocalDate(), searchStartTime, diff --git a/frontend/src/hooks/infiniteQuery/useNonLoginReservations.ts b/frontend/src/hooks/infiniteQuery/useNonLoginReservations.ts index b3fcf6d5e..fea2f7106 100644 --- a/frontend/src/hooks/infiniteQuery/useNonLoginReservations.ts +++ b/frontend/src/hooks/infiniteQuery/useNonLoginReservations.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { useInfiniteQuery } from 'react-query'; import { queryGuestNonLoginReservations } from '../../api/guestReservation'; +import { isNullish } from '../../utils/type'; const useNonLoginReservations = (userName: string, searchStartTime: string) => { const infiniteQueryResponse = useInfiniteQuery( @@ -15,6 +16,9 @@ const useNonLoginReservations = (userName: string, searchStartTime: string) => { { getNextPageParam: (response) => response.data.hasNext, refetchOnWindowFocus: false, + refetchOnMount: false, + refetchInterval: false, + refetchOnReconnect: false, } ); diff --git a/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx b/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx index b98350717..866a3dd45 100644 --- a/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx +++ b/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx @@ -16,12 +16,7 @@ const GuestNonLoginReservationSearch = (): JSX.Element => { const [{ userName }, onChangeForm] = useInputs
({ userName: '', }); - const [searchStartTime, setSearchStartTime] = useState(() => { - const nowDateTime = new Date(); - return `${formatDate(nowDateTime)}T${formatTimeWithSecond(nowDateTime)}${ - DATE.TIMEZONE_OFFSET_QUERY_STRING - }`; - }); + const [searchStartTime, setSearchStartTime] = useState(null); const handleSubmit: FormEventHandler = (event) => { event.preventDefault(); @@ -46,6 +41,7 @@ const GuestNonLoginReservationSearch = (): JSX.Element => { name="userName" value={userName} onChange={onChangeForm} + message="지난 예약은 조회되지 않습니다." autoFocus required /> @@ -53,10 +49,12 @@ const GuestNonLoginReservationSearch = (): JSX.Element => { 찾기 - + {searchStartTime !== null && ( + + )} ); diff --git a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts index c0ea4a078..b58a4c2b6 100644 --- a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts +++ b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts @@ -41,3 +41,21 @@ export const Form = styled.form` margin-bottom: 3rem; } `; + +export const PageHeader = styled.h2` + font-size: 1.5rem; + font-weight: 700; + margin: 0.75rem 0 1.25rem; +`; + +export const Image = styled.img` + width: 15%; +`; + +export const NotFoundContainer = styled.div` + padding: 60px 0 80px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +`; diff --git a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx index 1bc0b4438..589512a8c 100644 --- a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx +++ b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx @@ -1,8 +1,9 @@ import { AxiosError } from 'axios'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; import { useHistory } from 'react-router-dom'; import { deleteGuestReservation, queryGuestNonLoginReservations } from 'api/guestReservation'; +import GrayLogoImage from 'assets/images/gray-logo.png'; import { ReactComponent as DeleteIcon } from 'assets/svg/delete.svg'; import { ReactComponent as EditIcon } from 'assets/svg/edit.svg'; import Button from 'components/Button/Button'; @@ -30,20 +31,15 @@ const GuestNonLoginReservationSearchResult = ({ const history = useHistory(); const [passwordInputModalOpen, setPasswordInputModalOpen] = useState(false); const [selectedReservation, setSelectedReservation] = useState(); - const [currentSearchStartTime, setCurrentSearchStartTime] = useState(''); const { refetch, + isLoading: isLoadingReservations, fetchNextPage: fetchNextReservations, hasNextPage: hasNextReservations, flattedResults: flattedReservations, } = useNonLoginReservations(userName, searchStartTime); - if (searchStartTime !== currentSearchStartTime) { - refetch(); - setCurrentSearchStartTime(searchStartTime); - } - const removeReservation = useMutation(deleteGuestReservation, { onSuccess: () => { refetch(); @@ -97,9 +93,19 @@ const GuestNonLoginReservationSearchResult = ({ }); }; + useEffect(() => { + refetch(); + }, [refetch, searchStartTime]); + return ( <> + {!isLoadingReservations && flattedReservations.length === 0 && ( + + + 검색 결과가 없습니다. + + )} {flattedReservations.map((reservation) => ( Date: Sun, 12 Mar 2023 19:05:47 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20?= =?UTF-8?q?=EB=B0=8F=20API=20=EC=97=B0=EB=8F=99=20=EA=B5=AC=ED=98=84=20(#9?= =?UTF-8?q?24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내 정보 수정 페이지 UI 및 API 연동 구현 * refactor: ProfileEditForm의 onSubmit prop이 required가 되도록 수정 --- frontend/src/api/member.ts | 12 ++ .../EmojiSelector}/EmojiSelector.styles.ts | 0 .../EmojiSelector}/EmojiSelector.tsx | 9 +- .../MemberInfo/MemberInfo.styled.ts | 6 +- .../src/components/MemberInfo/MemberInfo.tsx | 11 +- frontend/src/constants/message.ts | 4 + frontend/src/constants/path.ts | 1 + frontend/src/constants/routes.tsx | 6 + .../src/pages/ManagerJoin/units/JoinForm.tsx | 2 +- .../ManagerProfileEdit.styles.ts | 32 +++++ .../ManagerProfileEdit/ManagerProfileEdit.tsx | 48 ++++++++ .../units/ProfileEditForm.styles.ts | 9 ++ .../units/ProfileEditForm.tsx | 109 ++++++++++++++++++ .../units/SocialJoinForm.tsx | 2 +- 14 files changed, 243 insertions(+), 8 deletions(-) rename frontend/src/{pages/ManagerJoin/units => components/EmojiSelector}/EmojiSelector.styles.ts (100%) rename frontend/src/{pages/ManagerJoin/units => components/EmojiSelector}/EmojiSelector.tsx (75%) create mode 100644 frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.styles.ts create mode 100644 frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx create mode 100644 frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts create mode 100644 frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx diff --git a/frontend/src/api/member.ts b/frontend/src/api/member.ts index 63238e2e4..a1ce3c9ad 100644 --- a/frontend/src/api/member.ts +++ b/frontend/src/api/member.ts @@ -11,6 +11,18 @@ export interface QueryMemberSuccess { organization: string | null; } +export interface PutMemberParams { + userName: string; + emoji: string; +} + export const queryMember: QueryFunction> = () => { return api.get('/members/me'); }; + +export const putMember = ({ userName, emoji }: PutMemberParams) => { + return api.put('/members/me', { + userName, + emoji, + }); +}; diff --git a/frontend/src/pages/ManagerJoin/units/EmojiSelector.styles.ts b/frontend/src/components/EmojiSelector/EmojiSelector.styles.ts similarity index 100% rename from frontend/src/pages/ManagerJoin/units/EmojiSelector.styles.ts rename to frontend/src/components/EmojiSelector/EmojiSelector.styles.ts diff --git a/frontend/src/pages/ManagerJoin/units/EmojiSelector.tsx b/frontend/src/components/EmojiSelector/EmojiSelector.tsx similarity index 75% rename from frontend/src/pages/ManagerJoin/units/EmojiSelector.tsx rename to frontend/src/components/EmojiSelector/EmojiSelector.tsx index 90605130f..5ace61074 100644 --- a/frontend/src/pages/ManagerJoin/units/EmojiSelector.tsx +++ b/frontend/src/components/EmojiSelector/EmojiSelector.tsx @@ -1,20 +1,24 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import useEmojiList from 'hooks/query/useEmojiList'; import * as Styled from './EmojiSelector.styles'; interface EmojiSelectorProps { + initialEmoji?: string; onSelect?: (emoji: string) => void; } -const EmojiSelector = ({ onSelect }: EmojiSelectorProps): JSX.Element => { +const EmojiSelector = ({ initialEmoji, onSelect }: EmojiSelectorProps): JSX.Element => { const emojiListQuery = useEmojiList(); + const [selectedEmoji, setSelectedEmoji] = useState(initialEmoji ?? null); + const emojiList = useMemo( () => emojiListQuery.data?.data.emojis ?? [], [emojiListQuery.data?.data.emojis] ); const handleSelect = (emoji: string) => { + setSelectedEmoji(emoji); onSelect?.(emoji); }; @@ -28,6 +32,7 @@ const EmojiSelector = ({ onSelect }: EmojiSelectorProps): JSX.Element => { type="radio" name="emoji" id={emoji.name} + checked={selectedEmoji === emoji.name} onChange={() => handleSelect(emoji.name)} /> {emoji.code} diff --git a/frontend/src/components/MemberInfo/MemberInfo.styled.ts b/frontend/src/components/MemberInfo/MemberInfo.styled.ts index f3bcd6d61..edb2e903c 100644 --- a/frontend/src/components/MemberInfo/MemberInfo.styled.ts +++ b/frontend/src/components/MemberInfo/MemberInfo.styled.ts @@ -27,7 +27,7 @@ export const InfoContainer = styled.div` flex-direction: column; & > * { - margin-bottom: 12px; + margin-bottom: 6px; ::last-of-type { margin-bottom: 0; @@ -37,6 +37,8 @@ export const InfoContainer = styled.div` export const NameTextContainer = styled.div` font-size: 1.5rem; + display: flex; + align-items: center; `; export const NameText = styled.span` @@ -44,6 +46,6 @@ export const NameText = styled.span` margin-right: 0.375rem; `; -export const OranizationTextContainer = styled.div` +export const OrganizationTextContainer = styled.div` font-size: 0.875rem; `; diff --git a/frontend/src/components/MemberInfo/MemberInfo.tsx b/frontend/src/components/MemberInfo/MemberInfo.tsx index 173fc4b37..95afd8433 100644 --- a/frontend/src/components/MemberInfo/MemberInfo.tsx +++ b/frontend/src/components/MemberInfo/MemberInfo.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Link } from 'react-router-dom'; +import { theme } from 'App.styles'; +import { ReactComponent as CaretIcon } from 'assets/svg/caret-right.svg'; +import PATH from 'constants/path'; import useMember from 'hooks/query/useMember'; import * as Styled from './MemberInfo.styled'; @@ -13,11 +17,14 @@ const MemberInfo = (): JSX.Element => { {data?.data.userName}님 + + + {data?.data.organization && ( - + {data?.data.organization} - + )} diff --git a/frontend/src/constants/message.ts b/frontend/src/constants/message.ts index 95d949bbe..f2df4e279 100644 --- a/frontend/src/constants/message.ts +++ b/frontend/src/constants/message.ts @@ -19,6 +19,10 @@ const MESSAGE = { LOGIN: { UNEXPECTED_ERROR: '로그인에 문제가 발생했습니다. 잠시 후에 다시 시도해주세요.', }, + MEMBER: { + EDIT_PROFILE_UNEXPECTED_ERROR: + '내 정보를 수정하는데 문제가 발생했습니다. 잠시 후에 다시 시도해주세요.', + }, RESERVATION: { CREATE: '예약하기', EDIT: '예약 수정하기', diff --git a/frontend/src/constants/path.ts b/frontend/src/constants/path.ts index 026903c23..1d341ea84 100644 --- a/frontend/src/constants/path.ts +++ b/frontend/src/constants/path.ts @@ -27,6 +27,7 @@ const PATH = { MANAGER_SOCIAL_JOIN: '/join/social', MANAGER_GITHUB_OAUTH_REDIRECT: '/login/oauth/github', MANAGER_GOOGLE_OAUTH_REDIRECT: '/login/oauth/google', + MANAGER_PROFILE_EDIT: '/profile/edit', MANAGER_MAP_DETAIL: '/map/:mapId', MANAGER_MAP_LIST: '/map/list', MANAGER_MAP_CREATE: '/map/create', diff --git a/frontend/src/constants/routes.tsx b/frontend/src/constants/routes.tsx index 7adda95d0..b0c4ac0cd 100644 --- a/frontend/src/constants/routes.tsx +++ b/frontend/src/constants/routes.tsx @@ -2,6 +2,7 @@ import React, { ReactNode } from 'react'; import GuestMain from 'pages/GuestMain/GuestMain'; import GuestMapContainer from 'pages/GuestMap/GuestMapContainer'; import ManagerMapList from 'pages/ManagerMapList/ManagerMapList'; +import ManagerProfileEdit from 'pages/ManagerProfileEdit/ManagerProfileEdit'; import PATH from './path'; const GuestMap = React.lazy(() => import('pages/GuestMap/GuestMap')); @@ -85,6 +86,11 @@ export const PRIVATE_ROUTES: PrivateRoute[] = [ component: , redirectPath: PATH.LOGIN, }, + { + path: PATH.MANAGER_PROFILE_EDIT, + component: , + redirectPath: PATH.LOGIN, + }, { path: PATH.MANAGER_RESERVATION, component: , diff --git a/frontend/src/pages/ManagerJoin/units/JoinForm.tsx b/frontend/src/pages/ManagerJoin/units/JoinForm.tsx index dd1ed64da..a9678939b 100644 --- a/frontend/src/pages/ManagerJoin/units/JoinForm.tsx +++ b/frontend/src/pages/ManagerJoin/units/JoinForm.tsx @@ -3,6 +3,7 @@ import React, { FormEventHandler, useEffect, useState } from 'react'; import { useQuery } from 'react-query'; import { queryValidateEmail, queryValidateUserName } from 'api/join'; import Button from 'components/Button/Button'; +import EmojiSelector from 'components/EmojiSelector/EmojiSelector'; import Input from 'components/Input/Input'; import MANAGER from 'constants/manager'; import MESSAGE from 'constants/message'; @@ -10,7 +11,6 @@ import REGEXP from 'constants/regexp'; import useInputs from 'hooks/useInputs'; import { ErrorResponse } from 'types/response'; import { JoinParams } from '../ManagerJoin'; -import EmojiSelector from './EmojiSelector'; import * as Styled from './JoinForm.styles'; interface Form { diff --git a/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.styles.ts b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.styles.ts new file mode 100644 index 000000000..e06d4da17 --- /dev/null +++ b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.styles.ts @@ -0,0 +1,32 @@ +import styled from 'styled-components'; +import { FORM_MAX_WIDTH } from 'constants/style'; + +export const Container = styled.div` + width: 100%; + max-width: ${FORM_MAX_WIDTH}; + margin: 0 auto; +`; + +export const PageTitle = styled.h2` + font-size: 1.5rem; + font-weight: 400; + text-align: center; + margin: 2.125rem auto; +`; + +export const PasswordChangeLinkMessage = styled.p` + margin: 1rem 0; + text-align: center; + font-size: 0.75rem; + color: ${({ theme }) => theme.gray[500]}; + + a { + color: ${({ theme }) => theme.primary[400]}; + text-decoration: none; + margin-left: 0.375rem; + + &:hover { + font-weight: 700; + } + } +`; diff --git a/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx new file mode 100644 index 000000000..c293f66fc --- /dev/null +++ b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx @@ -0,0 +1,48 @@ +import { AxiosError } from 'axios'; +import React from 'react'; +import { useMutation } from 'react-query'; +import { Link, useHistory } from 'react-router-dom'; +import { putMember } from 'api/member'; +import Header from 'components/Header/Header'; +import Layout from 'components/Layout/Layout'; +import MESSAGE from 'constants/message'; +import PATH from 'constants/path'; +import { ErrorResponse } from 'types/response'; +import * as Styled from './ManagerProfileEdit.styles'; +import ProfileEditForm from './units/ProfileEditForm'; + +const ManagerProfileEdit = () => { + const history = useHistory(); + + const editProfile = useMutation(putMember, { + onSuccess: () => { + history.push(PATH.MANAGER_MAP_LIST); + }, + + onError: (error: AxiosError) => { + alert(error?.response?.data.message ?? MESSAGE.MEMBER.EDIT_PROFILE_UNEXPECTED_ERROR); + }, + }); + + const handleSubmit = ({ userName, emoji }: { userName: string; emoji: string }) => { + editProfile.mutate({ userName, emoji }); + }; + + return ( + <> +
+ + + 내 정보 수정 + + + 비밀번호를 변경하고 싶으신가요? + 변경하기 + + + + + ); +}; + +export default ManagerProfileEdit; diff --git a/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts new file mode 100644 index 000000000..413d3fac3 --- /dev/null +++ b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const Form = styled.form` + margin: 3.75rem 0 1rem; +`; + +export const InputWrapper = styled.div` + margin-bottom: 3rem; +`; diff --git a/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx new file mode 100644 index 000000000..149874829 --- /dev/null +++ b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx @@ -0,0 +1,109 @@ +import { AxiosError } from 'axios'; +import React, { useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; +import { queryValidateUserName } from 'api/join'; +import Button from 'components/Button/Button'; +import EmojiSelector from 'components/EmojiSelector/EmojiSelector'; +import Input from 'components/Input/Input'; +import MANAGER from 'constants/manager'; +import MESSAGE from 'constants/message'; +import useMember from 'hooks/query/useMember'; +import useInputs from 'hooks/useInputs'; +import { ErrorResponse } from 'types/response'; +import * as Styled from './ProfileEditForm.styles'; + +interface ProfileEditFormProps { + onSubmit: ({ userName, emoji }: { userName: string; emoji: string }) => void; +} + +const ProfileEditForm = ({ onSubmit }: ProfileEditFormProps) => { + const member = useMember(); + const initialUserName = member.data?.data.userName; + const initialEmoji = member.data?.data.emoji.name; + + const [emoji, setEmoji] = useState(''); + const [{ userName }, onChangeForm, setInputs] = useInputs<{ userName: string }>({ + userName: '', + }); + + const [userNameMessage, setUserNameMessage] = useState(''); + + const checkValidateUserName = useQuery( + ['checkValidateUserName', userName], + queryValidateUserName, + { + enabled: false, + retry: false, + + onSuccess: () => { + setUserNameMessage(MESSAGE.JOIN.VALID_USERNAME); + }, + + onError: (error: AxiosError) => { + setUserNameMessage( + error.response?.data.message ?? MESSAGE.JOIN.CHECK_USERNAME_UNEXPECTED_ERROR + ); + }, + } + ); + + const handleSelectEmoji = (emoji: string) => { + setEmoji(emoji); + }; + + const handleChangeUserName = (event: React.ChangeEvent) => { + onChangeForm(event); + setUserNameMessage(''); + }; + + const handleSubmit: React.FormEventHandler = (event) => { + event.preventDefault(); + + onSubmit({ userName, emoji }); + }; + + const isSubmitButtonDisabled = !(emoji && userName); + + useEffect(() => { + if (!initialUserName) return; + + setInputs((prevInputs) => ({ + ...prevInputs, + userName: initialUserName, + })); + }, [initialUserName, setInputs]); + + useEffect(() => { + if (!initialEmoji) return; + + setEmoji(initialEmoji); + }, [initialEmoji]); + + return ( + + {initialEmoji && } + + {initialUserName && ( + + + + )} + + + ); +}; + +export default ProfileEditForm; diff --git a/frontend/src/pages/ManagerSocialJoin/units/SocialJoinForm.tsx b/frontend/src/pages/ManagerSocialJoin/units/SocialJoinForm.tsx index 04899836e..0a20ce837 100644 --- a/frontend/src/pages/ManagerSocialJoin/units/SocialJoinForm.tsx +++ b/frontend/src/pages/ManagerSocialJoin/units/SocialJoinForm.tsx @@ -2,12 +2,12 @@ import { AxiosError } from 'axios'; import { FormEventHandler, useState } from 'react'; import { useQuery } from 'react-query'; import { queryValidateUserName } from 'api/join'; +import EmojiSelector from 'components/EmojiSelector/EmojiSelector'; import Input from 'components/Input/Input'; import SocialJoinButton from 'components/SocialAuthButton/SocialJoinButton'; import MANAGER from 'constants/manager'; import MESSAGE from 'constants/message'; import useInput from 'hooks/useInput'; -import EmojiSelector from 'pages/ManagerJoin/units/EmojiSelector'; import { ErrorResponse } from 'types/response'; import { SocialJoinParams } from '../ManagerSocialJoin'; import * as Styled from './SocialJoinForm.styles'; From 61b50240faba959db298f1e44253cbc75b0a2f6f Mon Sep 17 00:00:00 2001 From: Shim MunSeong Date: Sun, 12 Mar 2023 19:23:59 +0900 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20iOS=EC=97=90=EC=84=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=9B=84=20=EC=98=88=EC=95=BD=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=ED=95=84=EB=93=9C=20=ED=9D=90=EB=A6=AC?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#925)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Input/Input.styles.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/Input/Input.styles.ts b/frontend/src/components/Input/Input.styles.ts index 0aad51464..efedfea39 100644 --- a/frontend/src/components/Input/Input.styles.ts +++ b/frontend/src/components/Input/Input.styles.ts @@ -70,7 +70,9 @@ export const Input = styled.input` } &:disabled { + -webkit-text-fill-color: ${({ theme }) => theme.gray[400]}; color: ${({ theme }) => theme.gray[400]}; + opacity: 1; } `; From bec7e392b156e697a51db68ea6abd80b5469cf2a Mon Sep 17 00:00:00 2001 From: Kimun Kim Date: Sun, 12 Mar 2023 19:45:35 +0900 Subject: [PATCH 07/15] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20url=20=EC=98=A4=EA=B8=B0=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 비밀번호 변경 테스트 요청 url 오기입 수정 * refactor: member, reservation dto에 NoArgs, AllArgs 생성자 생성 --- .../zzimkkong/dto/member/ProfileEmojiResponse.java | 4 ++++ .../reservation/ReservationInfiniteScrollResponse.java | 4 ++++ .../dto/reservation/ReservationOwnerResponse.java | 5 ++++- .../dto/space/SpaceFindAllAvailabilityResponse.java | 4 ++++ .../dto/space/SpaceFindAvailabilityResponse.java | 9 ++++++--- .../zzimkkong/controller/MemberControllerTest.java | 2 +- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ProfileEmojiResponse.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ProfileEmojiResponse.java index 4673c9ef9..cc6f6d96b 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ProfileEmojiResponse.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ProfileEmojiResponse.java @@ -1,11 +1,15 @@ package com.woowacourse.zzimkkong.dto.member; import com.woowacourse.zzimkkong.domain.ProfileEmoji; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class ProfileEmojiResponse { private ProfileEmoji name; private String code; diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationInfiniteScrollResponse.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationInfiniteScrollResponse.java index f6febcd28..cdaa4d9b2 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationInfiniteScrollResponse.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationInfiniteScrollResponse.java @@ -1,14 +1,18 @@ package com.woowacourse.zzimkkong.dto.reservation; import com.woowacourse.zzimkkong.domain.Reservation; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.List; import java.util.stream.Collectors; @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class ReservationInfiniteScrollResponse { private List data; private Boolean hasNext; diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationOwnerResponse.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationOwnerResponse.java index 47f95944b..82924acf4 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationOwnerResponse.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/reservation/ReservationOwnerResponse.java @@ -2,11 +2,12 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.woowacourse.zzimkkong.domain.Map; -import com.woowacourse.zzimkkong.domain.Member; import com.woowacourse.zzimkkong.domain.Reservation; import com.woowacourse.zzimkkong.domain.Space; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.ZonedDateTime; @@ -15,6 +16,8 @@ @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class ReservationOwnerResponse { private Long id; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATETIME_FORMAT) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAllAvailabilityResponse.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAllAvailabilityResponse.java index 9d0b93382..4eeaabd7c 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAllAvailabilityResponse.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAllAvailabilityResponse.java @@ -1,8 +1,10 @@ package com.woowacourse.zzimkkong.dto.space; import com.woowacourse.zzimkkong.domain.Space; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Collection; import java.util.List; @@ -11,6 +13,8 @@ @Builder @Getter +@NoArgsConstructor +@AllArgsConstructor public class SpaceFindAllAvailabilityResponse { private Long mapId; private List spaces; diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAvailabilityResponse.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAvailabilityResponse.java index 8a4faca75..befcb5601 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAvailabilityResponse.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/space/SpaceFindAvailabilityResponse.java @@ -1,15 +1,17 @@ package com.woowacourse.zzimkkong.dto.space; import com.woowacourse.zzimkkong.domain.Space; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Collection; -import java.util.Set; - -@Builder @Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class SpaceFindAvailabilityResponse { private Long spaceId; private Boolean isAvailable; @@ -21,3 +23,4 @@ public static SpaceFindAvailabilityResponse of(final Space space, final Collecti .build(); } } + diff --git a/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java b/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java index 96c34c20f..410303d4e 100644 --- a/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/woowacourse/zzimkkong/controller/MemberControllerTest.java @@ -267,7 +267,7 @@ private ExtractableResponse changePassword(ChangePasswordRequest chang .filter(document("member/password/put", getRequestPreprocessor(), getResponsePreprocessor())) .contentType(MediaType.APPLICATION_JSON_VALUE) .body(changePasswordRequest) - .when().put("/api/members/me/password/put") + .when().put("/api/members/me/password") .then().log().all().extract(); } From 874d9b9236d3e14b1afbacbcb66d2a6542458750 Mon Sep 17 00:00:00 2001 From: Kimun Kim Date: Sun, 12 Mar 2023 20:01:05 +0900 Subject: [PATCH 08/15] =?UTF-8?q?docs:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/docs/asciidoc/member.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index a0e815a50..11f095697 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -67,6 +67,12 @@ include::{snippets}/member/myinfo/put/http-request.adoc[] ==== Response include::{snippets}/member/myinfo/put/http-response.adoc[] +=== 비밀번호 변경 +==== Request +include::{snippets}/member/password/put/http-request.adoc[] +==== Response +include::{snippets}/member/password/put/http-response.adoc[] + === 회원 탈퇴 ==== Request include::{snippets}/member/myinfo/delete/http-request.adoc[] From c67951c20f7d23a2bff90a617db2036198c2a6e5 Mon Sep 17 00:00:00 2001 From: Sunny K Date: Sun, 12 Mar 2023 20:01:46 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=A2=80=20=EB=8D=94?= =?UTF-8?q?=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20(#920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/GuestMap/units/GuestMapDrawing.styles.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/pages/GuestMap/units/GuestMapDrawing.styles.ts b/frontend/src/pages/GuestMap/units/GuestMapDrawing.styles.ts index cea0efe6c..73e518e01 100644 --- a/frontend/src/pages/GuestMap/units/GuestMapDrawing.styles.ts +++ b/frontend/src/pages/GuestMap/units/GuestMapDrawing.styles.ts @@ -6,12 +6,8 @@ interface SpaceElementProps { const disabledCSS = css` opacity: 0.2; + filter: grayscale(1) brightness(3); pointer-events: none; - filter: brightness(0.5); - - &:hover { - opacity: 0.2; - } `; export const MapItem = styled.div<{ width: number; height: number }>` From b2a317018cf6a2ea39984525b692be086bedf9e1 Mon Sep 17 00:00:00 2001 From: JO YUN HO Date: Sun, 12 Mar 2023 20:27:19 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EC=9D=98=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=20=EA=B8=B0=EB=8A=A5=20(#928)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 비밀번호 경로 path, route 등록 * feat: 내 정보 수정에서 취소할 수 있는 기능 * feat: 비밀번호 변경 페이지 UI * chore: 필요하지 않는 import 제거 * feat: 비밀번호를 수정하는 기능 * feat: 이전 비밀번호 입력 시, 입력값이 마스킹되도록 처리 * feat: api 경로에 slash 추가 --- frontend/src/api/member.ts | 10 ++ frontend/src/constants/path.ts | 1 + frontend/src/constants/routes.tsx | 6 + .../ManagerPasswordEdit.styles.ts | 44 ++++++ .../ManagerPasswordEdit.tsx | 149 ++++++++++++++++++ .../ManagerProfileEdit/ManagerProfileEdit.tsx | 2 +- .../units/ProfileEditForm.styles.ts | 8 + .../units/ProfileEditForm.tsx | 19 ++- 8 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.styles.ts create mode 100644 frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.tsx diff --git a/frontend/src/api/member.ts b/frontend/src/api/member.ts index a1ce3c9ad..705b2a2bc 100644 --- a/frontend/src/api/member.ts +++ b/frontend/src/api/member.ts @@ -16,6 +16,12 @@ export interface PutMemberParams { emoji: string; } +export interface PutMemberPasswordParams { + oldPassword: string; + newPassword: string; + newPasswordConfirm: string; +} + export const queryMember: QueryFunction> = () => { return api.get('/members/me'); }; @@ -26,3 +32,7 @@ export const putMember = ({ userName, emoji }: PutMemberParams) => { emoji, }); }; + +export const putMemberPassword = (params: PutMemberPasswordParams) => { + return api.put('/members/me/password', params); +}; diff --git a/frontend/src/constants/path.ts b/frontend/src/constants/path.ts index 1d341ea84..31bbbcd9c 100644 --- a/frontend/src/constants/path.ts +++ b/frontend/src/constants/path.ts @@ -28,6 +28,7 @@ const PATH = { MANAGER_GITHUB_OAUTH_REDIRECT: '/login/oauth/github', MANAGER_GOOGLE_OAUTH_REDIRECT: '/login/oauth/google', MANAGER_PROFILE_EDIT: '/profile/edit', + MANAGER_PASSWORD_EDIT: '/password/edit', MANAGER_MAP_DETAIL: '/map/:mapId', MANAGER_MAP_LIST: '/map/list', MANAGER_MAP_CREATE: '/map/create', diff --git a/frontend/src/constants/routes.tsx b/frontend/src/constants/routes.tsx index b0c4ac0cd..c4532620b 100644 --- a/frontend/src/constants/routes.tsx +++ b/frontend/src/constants/routes.tsx @@ -2,6 +2,7 @@ import React, { ReactNode } from 'react'; import GuestMain from 'pages/GuestMain/GuestMain'; import GuestMapContainer from 'pages/GuestMap/GuestMapContainer'; import ManagerMapList from 'pages/ManagerMapList/ManagerMapList'; +import ManagerPasswordEdit from 'pages/ManagerPasswordEdit/ManagerPasswordEdit'; import ManagerProfileEdit from 'pages/ManagerProfileEdit/ManagerProfileEdit'; import PATH from './path'; @@ -91,6 +92,11 @@ export const PRIVATE_ROUTES: PrivateRoute[] = [ component: , redirectPath: PATH.LOGIN, }, + { + path: PATH.MANAGER_PASSWORD_EDIT, + component: , + redirectPath: PATH.LOGIN, + }, { path: PATH.MANAGER_RESERVATION, component: , diff --git a/frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.styles.ts b/frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.styles.ts new file mode 100644 index 000000000..eeb90dd83 --- /dev/null +++ b/frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.styles.ts @@ -0,0 +1,44 @@ +import styled from 'styled-components'; +import { FORM_MAX_WIDTH } from 'constants/style'; + +export const ContainerForm = styled.form` + width: 100%; + max-width: ${FORM_MAX_WIDTH}; + margin: 0 auto; +`; + +export const PageTitle = styled.h2` + font-size: 1.5rem; + font-weight: 400; + text-align: center; + margin: 2.125rem auto; +`; + +export const PasswordChangeLinkMessage = styled.p` + margin: 1rem 0; + text-align: center; + font-size: 0.75rem; + color: ${({ theme }) => theme.gray[500]}; + + a { + color: ${({ theme }) => theme.primary[400]}; + text-decoration: none; + margin-left: 0.375rem; + + &:hover { + font-weight: 700; + } + } +`; + +export const InputWrapper = styled.div` + margin-bottom: 2rem; +`; + +export const ButtonContainer = styled.div` + display: flex; + + *:first-child { + margin-right: 0.5rem; + } +`; diff --git a/frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.tsx b/frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.tsx new file mode 100644 index 000000000..aed0886bc --- /dev/null +++ b/frontend/src/pages/ManagerPasswordEdit/ManagerPasswordEdit.tsx @@ -0,0 +1,149 @@ +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; +import { useMutation } from 'react-query'; +import { Link, useHistory } from 'react-router-dom'; +import { putMemberPassword } from 'api/member'; +import Button from 'components/Button/Button'; +import Header from 'components/Header/Header'; +import Input from 'components/Input/Input'; +import Layout from 'components/Layout/Layout'; +import MANAGER from 'constants/manager'; +import MESSAGE from 'constants/message'; +import PATH from 'constants/path'; +import REGEXP from 'constants/regexp'; +import useInputs from 'hooks/useInputs'; +import { ErrorResponse } from 'types/response'; +import * as Styled from './ManagerPasswordEdit.styles'; + +interface Form { + prevPassword: string; + password: string; + passwordConfirm: string; +} + +const ManagerPasswordEdit = () => { + const history = useHistory(); + + const [{ prevPassword, password, passwordConfirm }, onChangeForm] = useInputs({ + prevPassword: '', + password: '', + passwordConfirm: '', + }); + + const [passwordMessage, setPasswordMessage] = useState(''); + const [passwordConfirmMessage, setPasswordConfirmMessage] = useState(''); + + const editPassword = useMutation(putMemberPassword, { + onSuccess: () => { + history.push(PATH.MANAGER_MAP_LIST); + }, + + onError: (error: AxiosError) => { + alert(error?.response?.data.message ?? MESSAGE.MEMBER.EDIT_PROFILE_UNEXPECTED_ERROR); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + editPassword.mutate({ + newPassword: password, + newPasswordConfirm: passwordConfirm, + oldPassword: prevPassword, + }); + }; + + const isValidPassword = REGEXP.PASSWORD.test(password); + const isValidPasswordConfirm = password === passwordConfirm; + + const isSubmitButtonDisabled = !( + prevPassword && + password && + passwordConfirm && + isValidPassword && + isValidPasswordConfirm + ); + + const handleCancel = () => { + history.push(PATH.MANAGER_PROFILE_EDIT); + }; + + useEffect(() => { + if (!password) return; + + setPasswordMessage( + isValidPassword ? MESSAGE.JOIN.VALID_PASSWORD : MESSAGE.JOIN.INVALID_PASSWORD + ); + }, [password, isValidPassword]); + + useEffect(() => { + if (!password || !passwordConfirm) return; + + setPasswordConfirmMessage( + password === passwordConfirm + ? MESSAGE.JOIN.VALID_PASSWORD_CONFIRM + : MESSAGE.JOIN.INVALID_PASSWORD_CONFIRM + ); + }, [password, passwordConfirm]); + + return ( + <> +
+ + + 내 비밀번호 수정 + + + + + + + + + + + + + + + + + ); +}; + +export default ManagerPasswordEdit; diff --git a/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx index c293f66fc..8e3a398a4 100644 --- a/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx +++ b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx @@ -37,7 +37,7 @@ const ManagerProfileEdit = () => { 비밀번호를 변경하고 싶으신가요? - 변경하기 + 변경하기 diff --git a/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts index 413d3fac3..db417c055 100644 --- a/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts +++ b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.styles.ts @@ -7,3 +7,11 @@ export const Form = styled.form` export const InputWrapper = styled.div` margin-bottom: 3rem; `; + +export const ButtonContainer = styled.div` + display: flex; + + *:first-child { + margin-right: 0.5rem; + } +`; diff --git a/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx index 149874829..29227cd42 100644 --- a/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx +++ b/frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx @@ -1,12 +1,14 @@ import { AxiosError } from 'axios'; import React, { useEffect, useState } from 'react'; import { useQuery } from 'react-query'; +import { useHistory } from 'react-router-dom'; import { queryValidateUserName } from 'api/join'; import Button from 'components/Button/Button'; import EmojiSelector from 'components/EmojiSelector/EmojiSelector'; import Input from 'components/Input/Input'; import MANAGER from 'constants/manager'; import MESSAGE from 'constants/message'; +import PATH from 'constants/path'; import useMember from 'hooks/query/useMember'; import useInputs from 'hooks/useInputs'; import { ErrorResponse } from 'types/response'; @@ -17,6 +19,8 @@ interface ProfileEditFormProps { } const ProfileEditForm = ({ onSubmit }: ProfileEditFormProps) => { + const history = useHistory(); + const member = useMember(); const initialUserName = member.data?.data.userName; const initialEmoji = member.data?.data.emoji.name; @@ -64,6 +68,10 @@ const ProfileEditForm = ({ onSubmit }: ProfileEditFormProps) => { const isSubmitButtonDisabled = !(emoji && userName); + const handleCancel = () => { + history.push(PATH.MANAGER_MAP_LIST); + }; + useEffect(() => { if (!initialUserName) return; @@ -99,9 +107,14 @@ const ProfileEditForm = ({ onSubmit }: ProfileEditFormProps) => { /> )} - + + + + ); }; From 0f9f550783d581860784d2852a7990558a8b30b5 Mon Sep 17 00:00:00 2001 From: Jungseok Sung Date: Sun, 12 Mar 2023 20:29:12 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20=EB=B9=84=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=A1=B0=ED=9A=8C=20loader=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#930)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 비회원 예약 조회 loader 추가 * fix: isLoading -> isFetching 사용 --- frontend/src/components/Loader/Loader.ts | 36 +++++++++++++++++++ .../GuestNonLoginReservationSearch.tsx | 2 +- ...tNonLoginReservationSearchResult.styles.ts | 10 ++++-- .../GuestNonLoginReservationSearchResult.tsx | 23 ++++++++---- 4 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Loader/Loader.ts diff --git a/frontend/src/components/Loader/Loader.ts b/frontend/src/components/Loader/Loader.ts new file mode 100644 index 000000000..2bb05b4df --- /dev/null +++ b/frontend/src/components/Loader/Loader.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +const Loader = styled.div` + width: 64px; + height: 64px; + position: relative; + background-repeat: no-repeat; + background-size: 16px 16px; + background-image: ${({ theme }) => ` + linear-gradient(${theme.gray[200] ?? '#E4E4E7'} 16px, transparent 0), + linear-gradient(${theme.primary[500] ?? '#FF7515'} 16px, transparent 0), + linear-gradient(${theme.primary[500] ?? '#FF7515'} 16px, transparent 0), + linear-gradient(${theme.gray[200] ?? '#E4E4E7'} 16px, transparent 0)`}; + background-position: left top, left bottom, right top, right bottom; + animation: rotate 1s linear infinite; + + @keyframes rotate { + 0% { + width: 64px; + height: 64px; + transform: rotate(0deg); + } + 50% { + width: 30px; + height: 30px; + transform: rotate(180deg); + } + 100% { + width: 64px; + height: 64px; + transform: rotate(360deg); + } + } +`; + +export default Loader; diff --git a/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx b/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx index 866a3dd45..4fb58e867 100644 --- a/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx +++ b/frontend/src/pages/GuestNonLoginReservationSearch/GuestNonLoginReservationSearch.tsx @@ -46,7 +46,7 @@ const GuestNonLoginReservationSearch = (): JSX.Element => { required /> {searchStartTime !== null && ( diff --git a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts index b58a4c2b6..7cd8557a5 100644 --- a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts +++ b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.styles.ts @@ -49,10 +49,16 @@ export const PageHeader = styled.h2` `; export const Image = styled.img` - width: 15%; + width: 40%; `; -export const NotFoundContainer = styled.div` +export const FlexCenter = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +export const StatusContainer = styled.div` padding: 60px 0 80px; display: flex; justify-content: center; diff --git a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx index 589512a8c..b33b5df84 100644 --- a/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx +++ b/frontend/src/pages/GuestNonLoginReservationSearch/units/GuestNonLoginReservationSearchResult.tsx @@ -8,6 +8,7 @@ import { ReactComponent as DeleteIcon } from 'assets/svg/delete.svg'; import { ReactComponent as EditIcon } from 'assets/svg/edit.svg'; import Button from 'components/Button/Button'; import IconButton from 'components/IconButton/IconButton'; +import Loader from 'components/Loader/Loader'; import MemberReservationListItem from 'components/MemberReservationListItem/MemberReservationListItem'; import MESSAGE from 'constants/message'; import { HREF } from 'constants/path'; @@ -34,7 +35,7 @@ const GuestNonLoginReservationSearchResult = ({ const { refetch, - isLoading: isLoadingReservations, + isFetching: isFetchingReservations, fetchNextPage: fetchNextReservations, hasNextPage: hasNextReservations, flattedResults: flattedReservations, @@ -97,15 +98,23 @@ const GuestNonLoginReservationSearchResult = ({ refetch(); }, [refetch, searchStartTime]); + console.log(isFetchingReservations); return ( <> - {!isLoadingReservations && flattedReservations.length === 0 && ( - - - 검색 결과가 없습니다. - - )} + + {!isFetchingReservations && flattedReservations.length === 0 && ( + + + 검색 결과가 없습니다. + + )} + {isFetchingReservations && ( + + + + )} + {flattedReservations.map((reservation) => ( Date: Sun, 12 Mar 2023 20:29:37 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20api=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=EC=9E=85=EC=8B=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EA=B5=AC=EC=B2=B4=ED=99=94,=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=B4=20=ED=8C=A8=ED=84=B4=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=EB=A5=BC=20=EC=8B=A4=EC=8B=9C=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=8C,=20dto=20=ED=8C=A8=ED=84=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 비밀번호 변경 요청에서 기존 비밀번호는 패턴을 검사하지 않음 * feat: 비밀변호 변경 요청에서 기존 비밀번호 오기입시 예외 메세지 구체적으로 변경 * test: ChangePasswordRequest dto 패턴 검사 테스트 작성 --- .../dto/member/ChangePasswordRequest.java | 5 ++-- .../member/PasswordMismatchException.java | 2 +- .../dto/ChangePasswordRequestTest.java | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java index 4e06d4b19..5e0368446 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/ChangePasswordRequest.java @@ -1,9 +1,9 @@ package com.woowacourse.zzimkkong.dto.member; -import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Getter; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @@ -12,8 +12,7 @@ @Getter @NoArgsConstructor public class ChangePasswordRequest { - @NotNull(message = EMPTY_MESSAGE) - @Pattern(regexp = MEMBER_PW_FORMAT, message = MEMBER_PW_MESSAGE) + @NotBlank(message = EMPTY_MESSAGE) private String oldPassword; @NotNull(message = EMPTY_MESSAGE) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java b/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java index 26036aaff..36f72b582 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/exception/member/PasswordMismatchException.java @@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus; public class PasswordMismatchException extends InputFieldException { - private static final String MESSAGE = "비밀번호가 일치하지 않습니다."; + private static final String MESSAGE = "기존 비밀번호가 일치하지 않습니다."; public PasswordMismatchException() { super(MESSAGE, HttpStatus.BAD_REQUEST, PASSWORD); diff --git a/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java b/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java index 547a825fb..893620942 100644 --- a/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java +++ b/backend/src/test/java/com/woowacourse/zzimkkong/dto/ChangePasswordRequestTest.java @@ -3,8 +3,11 @@ import com.woowacourse.zzimkkong.dto.member.ChangePasswordRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import static com.woowacourse.zzimkkong.dto.ValidatorMessage.EMPTY_MESSAGE; import static com.woowacourse.zzimkkong.dto.ValidatorMessage.MEMBER_PW_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; @@ -41,4 +44,26 @@ void blankNewPasswordConfirm(String newPasswordConfirm) { .anyMatch(violation -> violation.getMessage().equals(MEMBER_PW_MESSAGE))) .isTrue(); } + + @ParameterizedTest + @NullSource + @DisplayName("비밀번호 변경시 새 비밀번호 란에 빈 문자열이 들어오면 처리한다.") + void blankPassword(String password) { + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest("oldPassword1234", password, password); + + assertThat(getConstraintViolations(changePasswordRequest).stream() + .anyMatch(violation -> violation.getMessage().equals(EMPTY_MESSAGE))) + .isTrue(); + } + + @ParameterizedTest + @CsvSource(value = {"test1234!:false", "test1234:false", "1234test:false", "testtest:true", "12341234:true", "test123:true", "test1234test1234test1:true", "한글도실패1231:true"}, delimiter = ':') + @DisplayName("비밀번호 변경시 새 비밀번호 란에 옳지 않은 비밀번호 형식의 문자열이 들어오면 처리한다.") + void invalidPassword(String password, boolean flag) { + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest("oldPassword1234", password, password); + + assertThat(getConstraintViolations(changePasswordRequest).stream() + .anyMatch(violation -> violation.getMessage().equals(MEMBER_PW_MESSAGE))) + .isEqualTo(flag); + } } \ No newline at end of file From 0faed7b7880c8029f309c4c61a8bb5b2190656c5 Mon Sep 17 00:00:00 2001 From: JO YUN HO Date: Sun, 12 Mar 2023 20:32:20 +0900 Subject: [PATCH 13/15] chore: version up 2.0.0 -> 2.1.0 (#931) --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index c9769c2c6..903ef8431 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "zzimkkong-frontend", - "version": "2.0.0", + "version": "2.1.0", "main": "src/index.tsx", "license": "MIT", "homepage": "https://github.com/woowacourse-teams/2021-zzimkkong", From de9756afeb2c0345d2c46d64194370b2b82aa463 Mon Sep 17 00:00:00 2001 From: Jungseok Sung Date: Sun, 12 Mar 2023 20:55:13 +0900 Subject: [PATCH 14/15] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20oauth=20provider=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zzimkkong/dto/member/MemberFindResponse.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/MemberFindResponse.java b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/MemberFindResponse.java index b201fb01c..137f2d82a 100644 --- a/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/MemberFindResponse.java +++ b/backend/src/main/java/com/woowacourse/zzimkkong/dto/member/MemberFindResponse.java @@ -1,6 +1,7 @@ package com.woowacourse.zzimkkong.dto.member; import com.woowacourse.zzimkkong.domain.Member; +import com.woowacourse.zzimkkong.domain.OauthProvider; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,18 +13,21 @@ public class MemberFindResponse { private String userName; private ProfileEmojiResponse emoji; private String organization; + private OauthProvider oauthProvider; private MemberFindResponse( final Long id, final String email, final String userName, final ProfileEmojiResponse profileEmojiResponse, - final String organization) { + final String organization, + final OauthProvider oauthProvider) { this.id = id; this.email = email; this.userName = userName; this.emoji = profileEmojiResponse; this.organization = organization; + this.oauthProvider = oauthProvider; } public static MemberFindResponse from(final Member member) { @@ -32,6 +36,7 @@ public static MemberFindResponse from(final Member member) { member.getEmail(), member.getUserName(), ProfileEmojiResponse.from(member.getEmoji()), - member.getOrganization()); + member.getOrganization(), + member.getOauthProvider()); } } From 3942978276c513ecca48b038530ae344fe96639f Mon Sep 17 00:00:00 2001 From: JO YUN HO Date: Sun, 12 Mar 2023 20:59:51 +0900 Subject: [PATCH 15/15] =?UTF-8?q?feat:=20OAuth=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8,=20=EB=A7=81=ED=81=AC=EA=B0=80=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20(#935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/member.ts | 1 + .../ManagerProfileEdit/ManagerProfileEdit.tsx | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/member.ts b/frontend/src/api/member.ts index 705b2a2bc..56e34f8c7 100644 --- a/frontend/src/api/member.ts +++ b/frontend/src/api/member.ts @@ -9,6 +9,7 @@ export interface QueryMemberSuccess { userName: string; emoji: Emoji; organization: string | null; + oauthProvider: 'GOOGLE' | 'GITHUB' | null; } export interface PutMemberParams { diff --git a/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx index 8e3a398a4..449bd29bc 100644 --- a/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx +++ b/frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx @@ -7,6 +7,7 @@ import Header from 'components/Header/Header'; import Layout from 'components/Layout/Layout'; import MESSAGE from 'constants/message'; import PATH from 'constants/path'; +import useMember from 'hooks/query/useMember'; import { ErrorResponse } from 'types/response'; import * as Styled from './ManagerProfileEdit.styles'; import ProfileEditForm from './units/ProfileEditForm'; @@ -14,6 +15,10 @@ import ProfileEditForm from './units/ProfileEditForm'; const ManagerProfileEdit = () => { const history = useHistory(); + const member = useMember(); + + const isOAuthMember = member?.data?.data.oauthProvider !== null; + const editProfile = useMutation(putMember, { onSuccess: () => { history.push(PATH.MANAGER_MAP_LIST); @@ -35,10 +40,12 @@ const ManagerProfileEdit = () => { 내 정보 수정 - - 비밀번호를 변경하고 싶으신가요? - 변경하기 - + {!isOAuthMember && ( + + 비밀번호를 변경하고 싶으신가요? + 변경하기 + + )}