From 7e3d30d83ab4a0286656cf6a01f9c5549a2db6fd Mon Sep 17 00:00:00 2001 From: Redddy <78539407+reddevilmidzy@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:05:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[BE]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20API=20=EC=B6=94=EA=B0=80=20(#760)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 회원 탈퇴 기능 API 추가 * docs: 회원 탈퇴 API 문서 추가 * fix: 링크 삭제 테스트 id 하드코딩된 것 변경 * test: 사용하지 않는 메서드 삭제 * test: 회원 탈퇴 테스트 추가 * refactor: member를 조회 했을 시 없을 때 예외 처리 로직 service에서 repository로 이동 * refactor: 물리 삭제에서 논리 삭제로 변경 --- .../member/controller/MemberController.java | 13 +++- .../controller/docs/MemberControllerDocs.java | 10 +++ .../java/site/coduo/member/domain/Member.java | 19 +++++- .../domain/repository/MemberRepository.java | 18 ++++++ .../coduo/member/service/MemberService.java | 16 +++-- .../acceptance/MemberAcceptanceTest.java | 62 ++++++++++++++++--- .../member/service/MemberServiceTest.java | 26 ++++++++ 7 files changed, 148 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/site/coduo/member/controller/MemberController.java b/backend/src/main/java/site/coduo/member/controller/MemberController.java index e846b043..e0cafb98 100644 --- a/backend/src/main/java/site/coduo/member/controller/MemberController.java +++ b/backend/src/main/java/site/coduo/member/controller/MemberController.java @@ -4,6 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,11 +20,17 @@ public class MemberController implements MemberControllerDocs { private final MemberService memberService; @GetMapping("/member") - public ResponseEntity getMember( - @CookieValue(SIGN_IN_COOKIE_NAME) final String token - ) { + public ResponseEntity getMember(@CookieValue(SIGN_IN_COOKIE_NAME) final String token) { final MemberReadResponse response = memberService.findMemberNameByCredential(token); return ResponseEntity.ok(response); } + + @DeleteMapping("/member") + public ResponseEntity deleteMember(@CookieValue(SIGN_IN_COOKIE_NAME) final String token) { + memberService.deleteMember(token); + + return ResponseEntity.noContent() + .build(); + } } diff --git a/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java b/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java index ef0150df..de75df8f 100644 --- a/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java +++ b/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java @@ -29,4 +29,14 @@ ResponseEntity getMember( schema = @Schema(type = "string") ) String token); + + @ApiResponse(responseCode = "204", description = "회원 정보를 삭제한다.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) + ResponseEntity deleteMember( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "사용자가 인증에 성공하면 서버에서 발급하는 쿠키", + schema = @Schema(type = "string") + ) String token); } diff --git a/backend/src/main/java/site/coduo/member/domain/Member.java b/backend/src/main/java/site/coduo/member/domain/Member.java index fe3888c3..81bbb23a 100644 --- a/backend/src/main/java/site/coduo/member/domain/Member.java +++ b/backend/src/main/java/site/coduo/member/domain/Member.java @@ -1,5 +1,6 @@ package site.coduo.member.domain; +import java.time.LocalDateTime; import java.util.Objects; import jakarta.persistence.Column; @@ -9,6 +10,8 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.springframework.format.annotation.DateTimeFormat; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -41,14 +44,19 @@ public class Member extends BaseTimeEntity { @Column(name = "USER_NAME") private String username; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Column(name = "DELETED_AT") + private LocalDateTime deletedAt; + @Builder private Member(final String accessToken, final String loginId, final String userId, final String profileImage, - final String username) { + final String username, final LocalDateTime deletedAt) { this.accessToken = accessToken; this.loginId = loginId; this.userId = userId; this.profileImage = profileImage; this.username = username; + this.deletedAt = deletedAt; } public void update(final Member other) { @@ -57,6 +65,15 @@ public void update(final Member other) { this.userId = other.userId; this.profileImage = other.profileImage; this.username = other.username; + this.deletedAt = other.deletedAt; + } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } + + public boolean isDeleted() { + return deletedAt != null; } @Override diff --git a/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java b/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java index 0e1f72ea..af500af5 100644 --- a/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java @@ -1,15 +1,33 @@ package site.coduo.member.domain.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import site.coduo.member.domain.Member; +import site.coduo.member.exception.MemberNotFoundException; public interface MemberRepository extends JpaRepository { Optional findByUserId(String userId); + List findByDeletedAtIsNull(); + + @Override + default List findAll() { + return findByDeletedAtIsNull(); + } + + default Member fetchByUserId(final String userId) { + final Member member = findByUserId(userId) + .orElseThrow(() -> new MemberNotFoundException(String.format("%s는 찾을 수 없는 회원 아이디입니다.", userId))); + if (member.isDeleted()) { + throw new MemberNotFoundException(String.format("%s는 삭제된 회원입니다.", userId)); + } + return member; + } + boolean existsByUserId(String userId); } diff --git a/backend/src/main/java/site/coduo/member/service/MemberService.java b/backend/src/main/java/site/coduo/member/service/MemberService.java index 1212f648..f56b345b 100644 --- a/backend/src/main/java/site/coduo/member/service/MemberService.java +++ b/backend/src/main/java/site/coduo/member/service/MemberService.java @@ -10,7 +10,6 @@ import site.coduo.member.client.dto.GithubUserResponse; import site.coduo.member.domain.Member; import site.coduo.member.domain.repository.MemberRepository; -import site.coduo.member.exception.MemberNotFoundException; import site.coduo.member.infrastructure.http.Bearer; import site.coduo.member.infrastructure.security.JwtProvider; import site.coduo.member.service.dto.member.MemberReadResponse; @@ -36,15 +35,22 @@ public void createMember(final String username, final String encryptedAccessToke public MemberReadResponse findMemberNameByCredential(final String token) { final String userId = jwtProvider.extractSubject(token); - final Member member = memberRepository.findByUserId(userId) - .orElseThrow(() -> new MemberNotFoundException(String.format("%s는 찾을 수 없는 회원 아이디입니다.", userId))); + final Member member = memberRepository.fetchByUserId(userId); return new MemberReadResponse(member.getUsername()); } public Member findMemberByCredential(final String token) { final String userId = jwtProvider.extractSubject(token); - return memberRepository.findByUserId(userId) - .orElseThrow(() -> new MemberNotFoundException(String.format("%s는 찾을 수 없는 회원 아이디입니다.", userId))); + + return memberRepository.fetchByUserId(userId); + } + + @Transactional + public void deleteMember(final String token) { + final String userId = jwtProvider.extractSubject(token); + final Member member = memberRepository.fetchByUserId(userId); + + member.delete(); } } diff --git a/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java index 254e9244..f8cffac0 100644 --- a/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java @@ -50,19 +50,67 @@ void search_member_info() { .body("username", is(member.getUsername())); } - String login(Member member) { - final String sessionId = GithubAcceptanceTest.createAccessTokenCookie(); + @Test + @DisplayName("회원을 삭제한다.") + void delete_member() { + //given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String loginToken = jwtProvider.sign(member.getUserId()); memberRepository.save(member); - return RestAssured + //when && then + RestAssured .given() - .cookie("JSESSIONID", sessionId) + .cookie(SIGN_IN_COOKIE_NAME, loginToken) .when() - .get("/api/sign-in/callback") + .delete("/api/member") - .thenReturn() - .cookie("coudo_whoami"); + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + + @Test + @DisplayName("존재하지 않는 회원을 삭제한다.") + void delete_not_member() { + //given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + + final String loginToken = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + //when && then + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NOT_FOUND); } } diff --git a/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java b/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java index 4dddba76..ef8eead9 100644 --- a/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java +++ b/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -89,4 +91,28 @@ void search_member_by_login_token() { // then assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); } + + @Test + @DisplayName("회원을 삭제한다.") + void delete_member() { + // given + final Member member = Member.builder() + .userId("userid") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String token = jwtProvider.sign(member.getUserId()); + + memberRepository.save(member); + final List beforeDelete = memberRepository.findAll(); + + // when + memberService.deleteMember(token); + + //then + final List afterDelete = memberRepository.findAll(); + assertThat(afterDelete).hasSize(beforeDelete.size() - 1); + } } From fcc5e35257de9510cfbffcddd697648d930b5108 Mon Sep 17 00:00:00 2001 From: Redddy <78539407+reddevilmidzy@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:08:54 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20https://coduo.site=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 김민종 --- .../infrastructure/swagger/config/SwaggerConfig.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java b/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java index 3149102d..e3a08213 100644 --- a/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java +++ b/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; @Configuration public class SwaggerConfig { @@ -18,6 +19,7 @@ public OpenAPI openAPI() { return new OpenAPI() .info(apiInfo()) .components(securitySchemeComponents()) + .addServersItem(serverItem()) .addSecurityItem(securityRequirement()); } @@ -39,6 +41,12 @@ private Components securitySchemeComponents() { return new Components().addSecuritySchemes(HttpHeaders.AUTHORIZATION, bearerAuth); } + private Server serverItem() { + return new Server() + .url("https://coduo.site") + .description("클라우드에 배포된 서버"); + } + private SecurityRequirement securityRequirement() { return new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION); }