From 05cdc7ede6e1d1c73438a64c421acff6cfbfe2e7 Mon Sep 17 00:00:00 2001 From: yongckim Date: Tue, 26 Mar 2024 20:42:51 +0900 Subject: [PATCH 1/4] =?UTF-8?q?:recycle:=20[STMT-193]=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=EC=97=90=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stumeet/server/common/model/ApiResponse.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/stumeet/server/common/model/ApiResponse.java b/src/main/java/com/stumeet/server/common/model/ApiResponse.java index ecea7f9e..0c693f3d 100644 --- a/src/main/java/com/stumeet/server/common/model/ApiResponse.java +++ b/src/main/java/com/stumeet/server/common/model/ApiResponse.java @@ -20,6 +20,10 @@ public static ApiResponse success(SuccessCode code, T data) { return new ApiResponse<>(code.getHttpStatusCode(), code.getMessage(), data); } + public static ApiResponse success(SuccessCode code) { + return new ApiResponse<>(code.getHttpStatusCode(), code.getMessage(), null); + } + public static ApiResponse fail(ErrorCode errorCode) { return new ApiResponse<>( errorCode.getHttpStatusCode(), From ac4cb46a140da6dd37466e8bb1e02c6b3f9481dd Mon Sep 17 00:00:00 2001 From: yongckim Date: Tue, 26 Mar 2024 20:43:49 +0900 Subject: [PATCH 2/4] =?UTF-8?q?:recycle:=20[STMT-193]=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=ED=83=88=ED=87=B4=EC=8B=9C=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=84=B1=EA=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stumeet/server/common/response/SuccessCode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stumeet/server/common/response/SuccessCode.java b/src/main/java/com/stumeet/server/common/response/SuccessCode.java index 0b641ea1..90c47b1a 100644 --- a/src/main/java/com/stumeet/server/common/response/SuccessCode.java +++ b/src/main/java/com/stumeet/server/common/response/SuccessCode.java @@ -12,8 +12,8 @@ public enum SuccessCode { GET_SUCCESS(HttpStatus.OK, "조회에 성공했습니다."), POST_SUCCESS(HttpStatus.CREATED, "생성에 성공했습니다."), SIGN_UP_SUCCESS(HttpStatus.CREATED, "회원가입에 성공했습니다."), - FILE_UPLOAD_SUCCESS(HttpStatus.CREATED, "파일 업로드에 성공했습니다.") - ; + FILE_UPLOAD_SUCCESS(HttpStatus.CREATED, "파일 업로드에 성공했습니다."), + STUDY_LEAVE_SUCCESS(HttpStatus.OK, "스터디 탈퇴에 성공했습니다."); private final HttpStatus httpStatus; private final String message; From 02bbf9212a063ac67abee595216fd3754502c5cb Mon Sep 17 00:00:00 2001 From: yongckim Date: Tue, 26 Mar 2024 20:44:55 +0900 Subject: [PATCH 3/4] =?UTF-8?q?:sparkles:=20[STMT-193]=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=ED=83=88=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/StudyMemberLeaveApi.java | 31 +++++++++++++++++++ .../persistence/JpaStudyMemberRepository.java | 2 ++ .../StudyMemberLeavePersistenceAdapter.java | 16 ++++++++++ .../port/in/StudyMemberLeaveUseCase.java | 5 +++ .../port/out/StudyMemberLeavePort.java | 5 +++ .../service/StudyMemberLeaveService.java | 27 ++++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApi.java create mode 100644 src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberLeavePersistenceAdapter.java create mode 100644 src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberLeaveUseCase.java create mode 100644 src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberLeavePort.java create mode 100644 src/main/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveService.java diff --git a/src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApi.java b/src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApi.java new file mode 100644 index 00000000..0c488ebf --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApi.java @@ -0,0 +1,31 @@ +package com.stumeet.server.studymember.adapter.in.web; + +import com.stumeet.server.common.annotation.WebAdapter; +import com.stumeet.server.common.auth.model.LoginMember; +import com.stumeet.server.common.model.ApiResponse; +import com.stumeet.server.common.response.SuccessCode; +import com.stumeet.server.studymember.application.port.in.StudyMemberLeaveUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +@WebAdapter +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class StudyMemberLeaveApi { + + private final StudyMemberLeaveUseCase studyMemberLeaveUseCase; + + @DeleteMapping("/studies/{studyId}/members/me") + public ResponseEntity> leave( + @PathVariable Long studyId, + @AuthenticationPrincipal LoginMember member + ) { + studyMemberLeaveUseCase.leave(studyId, member.getMember().getId()); + return new ResponseEntity<>(ApiResponse.success(SuccessCode.STUDY_LEAVE_SUCCESS), HttpStatus.OK); + } +} diff --git a/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepository.java b/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepository.java index 527f0b12..4ced7846 100644 --- a/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepository.java +++ b/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepository.java @@ -4,4 +4,6 @@ public interface JpaStudyMemberRepository extends JpaRepository, JpaStudyMemberRepositoryCustom { long countByStudyId(Long studyId); + + void deleteByStudyIdAndMemberId(Long studyId, Long memberId); } diff --git a/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberLeavePersistenceAdapter.java b/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberLeavePersistenceAdapter.java new file mode 100644 index 00000000..f3c88f3c --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberLeavePersistenceAdapter.java @@ -0,0 +1,16 @@ +package com.stumeet.server.studymember.adapter.out.persistence; + +import com.stumeet.server.common.annotation.PersistenceAdapter; +import com.stumeet.server.studymember.application.port.out.StudyMemberLeavePort; +import lombok.RequiredArgsConstructor; + +@PersistenceAdapter +@RequiredArgsConstructor +public class StudyMemberLeavePersistenceAdapter implements StudyMemberLeavePort { + private final JpaStudyMemberRepository jpaStudyMemberRepository; + + @Override + public void leave(Long studyId, Long memberId) { + jpaStudyMemberRepository.deleteByStudyIdAndMemberId(studyId, memberId); + } +} diff --git a/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberLeaveUseCase.java b/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberLeaveUseCase.java new file mode 100644 index 00000000..fb2afede --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberLeaveUseCase.java @@ -0,0 +1,5 @@ +package com.stumeet.server.studymember.application.port.in; + +public interface StudyMemberLeaveUseCase { + void leave(Long studyId, Long memberId); +} diff --git a/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberLeavePort.java b/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberLeavePort.java new file mode 100644 index 00000000..7d5b3962 --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberLeavePort.java @@ -0,0 +1,5 @@ +package com.stumeet.server.studymember.application.port.out; + +public interface StudyMemberLeavePort { + void leave(Long studyId, Long memberId); +} diff --git a/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveService.java b/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveService.java new file mode 100644 index 00000000..6a835578 --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveService.java @@ -0,0 +1,27 @@ +package com.stumeet.server.studymember.application.service; + +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.study.application.port.in.StudyValidationUseCase; +import com.stumeet.server.studymember.application.port.in.StudyMemberLeaveUseCase; +import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; +import com.stumeet.server.studymember.application.port.out.StudyMemberLeavePort; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@Transactional +@RequiredArgsConstructor +public class StudyMemberLeaveService implements StudyMemberLeaveUseCase { + private final StudyValidationUseCase studyValidationUseCase; + private final StudyMemberValidationUseCase studyMemberValidationUseCase; + private final StudyMemberLeavePort studyMemberLeavePort; + + + @Override + public void leave(Long studyId, Long memberId) { + studyValidationUseCase.checkById(studyId); + studyMemberValidationUseCase.checkStudyJoinMember(studyId, memberId); + + studyMemberLeavePort.leave(studyId, memberId); + } +} From 868cc36903d9ba226e238e35de9d50215f468b1c Mon Sep 17 00:00:00 2001 From: yongckim Date: Tue, 26 Mar 2024 20:45:20 +0900 Subject: [PATCH 4/4] =?UTF-8?q?:white=5Fcheck=5Fmark:=20[STMT-193]=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=ED=83=88=ED=87=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/index.adoc | 26 +++++ .../in/web/StudyMemberLeaveApiTest.java | 97 +++++++++++++++++++ .../service/StudyMemberLeaveServiceTest.java | 77 +++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java create mode 100644 src/test/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveServiceTest.java diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 821bdced..4ca87aad 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -250,3 +250,29 @@ include::{snippets}/get-study-members/fail/not-joined-member/response-fields.ado ===== 응답 실패 (404) include::{snippets}/get-study-members/fail/not-exist-study/response-body.adoc[] include::{snippets}/get-study-members/fail/not-exist-study/response-fields.adoc[] + +=== 스터디 멤버 탈퇴 + +전달받은 스터디에 대해 탈퇴하는 API입니다. + +==== DELETE /v1/api/studies/{studyId}/members/me + +===== 요청 +include::{snippets}/leave-study/success/http-request.adoc[] +include::{snippets}/leave-study/success/request-headers.adoc[] +include::{snippets}/leave-study/success/path-parameters.adoc[] + +===== 응답 성공 (200) +.스터디 탈퇴 성공 +include::{snippets}/leave-study/success/response-body.adoc[] +include::{snippets}/leave-study/success/response-fields.adoc[] + +===== 응답 실패 (403) +.스터디에 가입한 멤버가 아닌 경우 +include::{snippets}/leave-study/fail/not-joined-member/response-body.adoc[] +include::{snippets}/leave-study/fail/not-joined-member/response-fields.adoc[] + +===== 응답 실패 (404) +.전달한 스터디가 존재하지 않는 경우 +include::{snippets}/leave-study/fail/not-exist-study/response-body.adoc[] +include::{snippets}/leave-study/fail/not-exist-study/response-fields.adoc[] \ No newline at end of file diff --git a/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java b/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java new file mode 100644 index 00000000..06d12473 --- /dev/null +++ b/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java @@ -0,0 +1,97 @@ +package com.stumeet.server.studymember.adapter.in.web; + +import com.stumeet.server.common.auth.model.AuthenticationHeader; +import com.stumeet.server.helper.WithMockMember; +import com.stumeet.server.stub.StudyStub; +import com.stumeet.server.stub.TokenStub; +import com.stumeet.server.template.ApiTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class StudyMemberLeaveApiTest extends ApiTest { + + @Nested + @DisplayName("스터디 멤버 탈퇴 API") + class Leave { + + private final String path = "/api/v1/studies/{studyId}/members/me"; + + @Test + @WithMockMember + @DisplayName("[성공] 스터디 멤버 탈퇴에 성공한다.") + void successTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + + mockMvc.perform(delete(path, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()) + .andDo(document("leave-study/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰")), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ))); + } + + @Test + @WithMockMember + @DisplayName("[실패] 스터디가 존재하지 않는 경우 스터디 멤버 탈퇴에 실패한다.") + void notExistStudyTest() throws Exception { + Long invalidStudyId = StudyStub.getInvalidStudyId(); + + mockMvc.perform(delete(path, invalidStudyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andDo(document("leave-study/fail/not-exist-study", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰")), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ))); + } + + @Test + @WithMockMember(id = 0L) + @DisplayName("[실패] 스터디에 가입된 멤버가 아닌 경우 스터디 멤버 탈퇴에 실패한다.") + void notStudyJoinMemberTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + + mockMvc.perform(delete(path, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isForbidden()) + .andDo(document("leave-study/fail/not-joined-member", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰")), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ))); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveServiceTest.java b/src/test/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveServiceTest.java new file mode 100644 index 00000000..ed1e5365 --- /dev/null +++ b/src/test/java/com/stumeet/server/studymember/application/service/StudyMemberLeaveServiceTest.java @@ -0,0 +1,77 @@ +package com.stumeet.server.studymember.application.service; + +import com.stumeet.server.stub.MemberStub; +import com.stumeet.server.stub.StudyStub; +import com.stumeet.server.study.application.port.in.StudyValidationUseCase; +import com.stumeet.server.study.domain.exception.StudyNotExistsException; +import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; +import com.stumeet.server.studymember.application.port.out.StudyMemberLeavePort; +import com.stumeet.server.studymember.domain.exception.StudyMemberNotJoinedException; +import com.stumeet.server.template.UnitTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willThrow; + +class StudyMemberLeaveServiceTest extends UnitTest { + + @InjectMocks + private StudyMemberLeaveService studyMemberLeaveService; + + @Mock + private StudyValidationUseCase studyValidationUseCase; + + @Mock + private StudyMemberValidationUseCase studyMemberValidationUseCase; + + @Mock + private StudyMemberLeavePort studyMemberLeavePort; + + @Nested + @DisplayName("스터디 멤버 탈퇴") + class Leave { + + @Test + @DisplayName("[성공] 스터디 멤버 탈퇴에 성공한다.") + void successTest() { + Long studyId = StudyStub.getStudyId(); + Long memberId = MemberStub.getMemberId(); + + studyMemberLeaveService.leave(studyId, memberId); + + then(studyMemberLeavePort).should().leave(any(), any()); + } + + @Test + @DisplayName("[실패] 스터디가 존재하지 않는 경우 예외가 발생한다.") + void studyNotFoundTest() { + Long studyId = StudyStub.getInvalidStudyId(); + Long memberId = MemberStub.getMemberId(); + + willThrow(StudyNotExistsException.class) + .given(studyValidationUseCase).checkById(studyId); + + assertThatCode(() -> studyMemberLeaveService.leave(studyId, memberId)) + .isInstanceOf(StudyNotExistsException.class); + } + + @Test + @DisplayName("[실패] 해당 스터디에 가입한 회원이 아닌 경우 예외가 발생한다.") + void notJoinMemberTest() { + Long studyId = StudyStub.getStudyId(); + Long memberId = MemberStub.getMemberId(); + + willThrow(StudyMemberNotJoinedException.class) + .given(studyMemberValidationUseCase).checkStudyJoinMember(studyId, memberId); + + assertThatCode(() -> studyMemberLeaveService.leave(studyId, memberId)) + .isInstanceOf(StudyMemberNotJoinedException.class); + } + } +} \ No newline at end of file