diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index e1fae914..67b595d3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -329,3 +329,32 @@ include::{snippets}/kick-study/fail/not-admin/response-fields.adoc[] ===== 응답 실패 (404) include::{snippets}/kick-study/fail/not-exist-study/response-body.adoc[] include::{snippets}/kick-study/fail/not-exist-study/response-fields.adoc[] + +=== 스터디 가입 + +멤버가 스터디에 가입할 때 사용하는 API 입니다. + +==== POST /v1/api/studies/{studyId}/members + +===== 요청 +include::{snippets}/study-member-join/success/http-request.adoc[] +include::{snippets}/study-member-join/success/path-parameters.adoc[] +include::{snippets}/study-member-join/success/request-headers.adoc[] + +===== 응답 성공 (201) +include::{snippets}/study-member-join/success/response-body.adoc[] +include::{snippets}/study-member-join/success/response-fields.adoc[] + +===== 응답 실패 (403) +.이미 스터디에 가입한 멤버인 경우 +include::{snippets}/study-member-join/fail/already-join-member/response-body.adoc[] +include::{snippets}/study-member-join/fail/already-join-member/response-fields.adoc[] + +===== 응답 실패 (404) +.존재하지 않는 스터디에 가입을 요청한 경우 +include::{snippets}/study-member-join/fail/not-exist-study/response-body.adoc[] +include::{snippets}/study-member-join/fail/not-exist-study/response-fields.adoc[] + +.존재하지 않는 멤버가 가입을 요청한 경우 +include::{snippets}/study-member-join/fail/not-exist-member/response-body.adoc[] +include::{snippets}/study-member-join/fail/not-exist-member/response-fields.adoc[] \ No newline at end of file diff --git a/src/main/java/com/stumeet/server/common/response/ErrorCode.java b/src/main/java/com/stumeet/server/common/response/ErrorCode.java index c54071e4..85817cbc 100644 --- a/src/main/java/com/stumeet/server/common/response/ErrorCode.java +++ b/src/main/java/com/stumeet/server/common/response/ErrorCode.java @@ -3,64 +3,65 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; - import org.springframework.http.HttpStatus; @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum ErrorCode { - /* - 400 - BAD REQUEST + /* + 400 - BAD REQUEST */ - VALIDATION_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), - BIND_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값을 바인딩하는 과정에서 오류가 발생하였습니다."), - METHOD_ARGUMENT_NOT_VALID_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 검증되지 않은 값 입니다."), - METHOD_ARGUMENT_TYPE_MISMATCH_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값의 타입이 잘못되었습니다."), - INVALID_FORMAT_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않은 데이터입니다."), - DUPLICATE_NICKNAME_EXCEPTION(HttpStatus.BAD_REQUEST, "닉네임이 중복되었습니다."), - NOT_EXIST_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 값이 존재하지 않습니다."), - NOT_MATCHED_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 토큰과 매칭되는 토큰이 없습니다."), - NOT_MATCHED_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 리프레시 토큰과 서버의 리프레시 토큰이 일치하지 않습니다."), - EXPIRED_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "리프레시 토큰이 만료되었습니다."), + VALIDATION_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + BIND_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값을 바인딩하는 과정에서 오류가 발생하였습니다."), + METHOD_ARGUMENT_NOT_VALID_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 검증되지 않은 값 입니다."), + METHOD_ARGUMENT_TYPE_MISMATCH_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값의 타입이 잘못되었습니다."), + INVALID_FORMAT_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않은 데이터입니다."), + DUPLICATE_NICKNAME_EXCEPTION(HttpStatus.BAD_REQUEST, "닉네임이 중복되었습니다."), + NOT_EXIST_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 값이 존재하지 않습니다."), + NOT_MATCHED_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 토큰과 매칭되는 토큰이 없습니다."), + NOT_MATCHED_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 리프레시 토큰과 서버의 리프레시 토큰이 일치하지 않습니다."), + EXPIRED_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "리프레시 토큰이 만료되었습니다."), - INVALID_FILE_NAME_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 이름입니다."), - INVALID_FILE_CONTENT_TYPE_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 컨텐트 타입 입니다."), - INVALID_FILE_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 확장자입니다."), + INVALID_FILE_NAME_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 이름입니다."), + INVALID_FILE_CONTENT_TYPE_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 컨텐트 타입 입니다."), + INVALID_FILE_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 확장자입니다."), - /* - 401 - UNAUTHORIZED - */ - JWT_INVALID_SIGNATURE_EXCEPTION(HttpStatus.UNAUTHORIZED, "JWT 서명이 유효하지 않습니다."), - ILLEGAL_KEY_ALGORITHM_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 키 알고리즘입니다."), - JWT_TOKEN_PARSING_EXCEPTION(HttpStatus.UNAUTHORIZED, "JWT 토큰 파싱에 실패했습니다."), - NOT_EXIST_OAUTH_PROVIDER(HttpStatus.UNAUTHORIZED, "존재하지 않는 OAuth 제공자입니다."), + /* + 401 - UNAUTHORIZED + */ + JWT_INVALID_SIGNATURE_EXCEPTION(HttpStatus.UNAUTHORIZED, "JWT 서명이 유효하지 않습니다."), + ILLEGAL_KEY_ALGORITHM_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 키 알고리즘입니다."), + JWT_TOKEN_PARSING_EXCEPTION(HttpStatus.UNAUTHORIZED, "JWT 토큰 파싱에 실패했습니다."), + NOT_EXIST_OAUTH_PROVIDER(HttpStatus.UNAUTHORIZED, "존재하지 않는 OAuth 제공자입니다."), - /* - 403 - FORBIDDEN - */ - ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "유효하지 않은 요청입니다."), - STUDY_MEMBER_NOT_JOINED_EXCEPTION(HttpStatus.FORBIDDEN, "스터디에 가입한 멤버가 아닙니다."), - NOT_STUDY_ADMIN_EXCEPTION(HttpStatus.FORBIDDEN, "스터디 관리자가 아닙니다."), + /* + 403 - FORBIDDEN + */ + ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "유효하지 않은 요청입니다."), + STUDY_MEMBER_NOT_JOINED_EXCEPTION(HttpStatus.FORBIDDEN, "스터디에 가입한 멤버가 아닙니다."), + NOT_STUDY_ADMIN_EXCEPTION(HttpStatus.FORBIDDEN, "스터디 관리자가 아닙니다."), + ALREADY_STUDY_JOIN_MEMBER_EXCEPTION(HttpStatus.FORBIDDEN, "스터디에 이미 가입한 사용자입니다."), - /* - 404 - NOT FOUND - */ - STUDY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 입니다."), - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 멤버 입니다."), - STUDY_FIELD_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 분야 입니다."), + /* + 404 - NOT FOUND + */ + STUDY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 입니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 멤버 입니다."), + STUDY_FIELD_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 분야 입니다."), - /* - 500 - INTERNAL SERVER ERROR - */ - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부에서 에러가 발생하였습니다."), - UPLOAD_FILE_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패하였습니다."), - NOT_IMPLEMENTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "구현되지 않은 메서드를 사용했습니다."); + /* + 500 - INTERNAL SERVER ERROR + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부에서 에러가 발생하였습니다."), + UPLOAD_FILE_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패하였습니다."), + NOT_IMPLEMENTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "구현되지 않은 메서드를 사용했습니다."), + ; - private final HttpStatus httpStatus; - private final String message; + private final HttpStatus httpStatus; + private final String message; - public int getHttpStatusCode() { - return httpStatus.value(); - } + public int getHttpStatusCode() { + return httpStatus.value(); + } } 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 fac1ef56..5637c239 100644 --- a/src/main/java/com/stumeet/server/common/response/SuccessCode.java +++ b/src/main/java/com/stumeet/server/common/response/SuccessCode.java @@ -22,7 +22,8 @@ public enum SuccessCode { POST_SUCCESS(HttpStatus.CREATED, "생성에 성공했습니다."), SIGN_UP_SUCCESS(HttpStatus.CREATED, "회원가입에 성공했습니다."), FILE_UPLOAD_SUCCESS(HttpStatus.CREATED, "파일 업로드에 성공했습니다."), - STUDY_CREATE_SUCCESS(HttpStatus.CREATED, "스터디 그룹 생성에 성공했습니다."); + STUDY_CREATE_SUCCESS(HttpStatus.CREATED, "스터디 그룹 생성에 성공했습니다."), + STUDY_JOIN_SUCCESS(HttpStatus.CREATED, "스터디 가입에 성공했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberJoinApi.java b/src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberJoinApi.java new file mode 100644 index 00000000..5a4731c2 --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberJoinApi.java @@ -0,0 +1,37 @@ +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.adapter.in.web.mapper.StudyMemberJoinWebAdapterMapper; +import com.stumeet.server.studymember.application.port.in.StudyMemberJoinUseCase; +import com.stumeet.server.studymember.application.port.in.command.StudyMemberJoinCommand; +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.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@WebAdapter +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class StudyMemberJoinApi { + + private final StudyMemberJoinUseCase studyMemberJoinUseCase; + private final StudyMemberJoinWebAdapterMapper studyMemberJoinWebAdapterMapper; + + @PostMapping("/studies/{studyId}/members") + public ResponseEntity> join( + @PathVariable Long studyId, + @AuthenticationPrincipal LoginMember member + ) { + StudyMemberJoinCommand command = studyMemberJoinWebAdapterMapper.toCommand(studyId, member.getMember().getId()); + studyMemberJoinUseCase.join(command); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.STUDY_JOIN_SUCCESS)); + } +} diff --git a/src/main/java/com/stumeet/server/studymember/adapter/in/web/mapper/StudyMemberJoinWebAdapterMapper.java b/src/main/java/com/stumeet/server/studymember/adapter/in/web/mapper/StudyMemberJoinWebAdapterMapper.java new file mode 100644 index 00000000..043799cc --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/adapter/in/web/mapper/StudyMemberJoinWebAdapterMapper.java @@ -0,0 +1,15 @@ +package com.stumeet.server.studymember.adapter.in.web.mapper; + +import com.stumeet.server.studymember.application.port.in.command.StudyMemberJoinCommand; +import org.springframework.stereotype.Component; + +@Component +public class StudyMemberJoinWebAdapterMapper { + public StudyMemberJoinCommand toCommand(Long studyId, Long memberId) { + return StudyMemberJoinCommand.builder() + .studyId(studyId) + .memberId(memberId) + .isAdmin(false) + .build(); + } +} diff --git a/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberPersistenceAdapter.java b/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberPersistenceAdapter.java index 39f4e8b0..544984f3 100644 --- a/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberPersistenceAdapter.java +++ b/src/main/java/com/stumeet/server/studymember/adapter/out/persistence/StudyMemberPersistenceAdapter.java @@ -33,6 +33,11 @@ public boolean isNotStudyJoinMember(Long studyId, Long memberId) { return !jpaStudyMemberRepository.isStudyJoinMember(studyId, memberId); } + @Override + public boolean isAlreadyStudyJoinMember(Long studyId, Long memberId) { + return jpaStudyMemberRepository.isStudyJoinMember(studyId, memberId); + } + @Override public boolean isNotAdmin(Long studyId, Long adminId) { return !jpaStudyMemberRepository.isAdmin(studyId, adminId); diff --git a/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberValidationUseCase.java b/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberValidationUseCase.java index 7f5defea..a5f62d39 100644 --- a/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberValidationUseCase.java +++ b/src/main/java/com/stumeet/server/studymember/application/port/in/StudyMemberValidationUseCase.java @@ -3,5 +3,7 @@ public interface StudyMemberValidationUseCase { void checkStudyJoinMember(Long studyId, Long memberId); + void checkAlreadyStudyJoinMember(Long studyId, Long memberId); + void checkAdmin(Long studyId, Long adminId); } diff --git a/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberValidationPort.java b/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberValidationPort.java index be55a8f2..7f2be169 100644 --- a/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberValidationPort.java +++ b/src/main/java/com/stumeet/server/studymember/application/port/out/StudyMemberValidationPort.java @@ -3,5 +3,7 @@ public interface StudyMemberValidationPort { boolean isNotStudyJoinMember(Long studyId, Long memberId); + boolean isAlreadyStudyJoinMember(Long studyId, Long memberId); + boolean isNotAdmin(Long studyId, Long adminId); } diff --git a/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberJoinService.java b/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberJoinService.java index b6764228..e35f018c 100644 --- a/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberJoinService.java +++ b/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberJoinService.java @@ -4,6 +4,7 @@ import com.stumeet.server.member.application.port.in.MemberValidationUseCase; import com.stumeet.server.study.application.port.in.StudyValidationUseCase; import com.stumeet.server.studymember.application.port.in.StudyMemberJoinUseCase; +import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; import com.stumeet.server.studymember.application.port.in.command.StudyMemberJoinCommand; import com.stumeet.server.studymember.application.port.in.mapper.StudyMemberUseCaseMapper; import com.stumeet.server.studymember.application.port.out.StudyMemberJoinPort; @@ -20,12 +21,16 @@ public class StudyMemberJoinService implements StudyMemberJoinUseCase { private final StudyValidationUseCase studyValidationUseCase; private final StudyMemberUseCaseMapper studyMemberUseCaseMapper; private final StudyMemberJoinPort studyMemberJoinPort; + private final StudyMemberValidationUseCase studyMemberValidationUseCase; @Override public void join(StudyMemberJoinCommand command) { memberValidationUseCase.checkById(command.memberId()); studyValidationUseCase.checkById(command.studyId()); + studyMemberValidationUseCase.checkAlreadyStudyJoinMember(command.studyId(), command.memberId()); + StudyMember studyMember = studyMemberUseCaseMapper.toDomain(command); + studyMemberJoinPort.join(studyMember); } } diff --git a/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberValidationService.java b/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberValidationService.java index 7fe92023..ce3641c3 100644 --- a/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberValidationService.java +++ b/src/main/java/com/stumeet/server/studymember/application/service/StudyMemberValidationService.java @@ -3,6 +3,7 @@ import com.stumeet.server.common.annotation.UseCase; import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; import com.stumeet.server.studymember.application.port.out.StudyMemberValidationPort; +import com.stumeet.server.studymember.domain.exception.AlreadyStudyJoinMemberException; import com.stumeet.server.studymember.domain.exception.NotStudyAdminException; import com.stumeet.server.studymember.domain.exception.StudyMemberNotJoinedException; import lombok.RequiredArgsConstructor; @@ -18,14 +19,21 @@ public class StudyMemberValidationService implements StudyMemberValidationUseCas @Override public void checkStudyJoinMember(Long studyId, Long memberId) { if (studyMemberValidationPort.isNotStudyJoinMember(studyId, memberId)) { - throw new StudyMemberNotJoinedException("스터디에 가입된 멤버가 아닙니다. 전달받은 studyId=" + studyId + ", memberId=" + memberId); + throw new StudyMemberNotJoinedException(studyId, memberId); + } + } + + @Override + public void checkAlreadyStudyJoinMember(Long studyId, Long memberId) { + if (studyMemberValidationPort.isAlreadyStudyJoinMember(studyId, memberId)) { + throw new AlreadyStudyJoinMemberException(studyId, memberId); } } @Override public void checkAdmin(Long studyId, Long adminId) { if (studyMemberValidationPort.isNotAdmin(studyId, adminId)) { - throw new NotStudyAdminException("스터디 관리자가 아닙니다. 전달받은 studyId=" + studyId + ", adminId=" + adminId); + throw new NotStudyAdminException(studyId, adminId); } } } diff --git a/src/main/java/com/stumeet/server/studymember/domain/exception/AlreadyStudyJoinMemberException.java b/src/main/java/com/stumeet/server/studymember/domain/exception/AlreadyStudyJoinMemberException.java new file mode 100644 index 00000000..a7b73120 --- /dev/null +++ b/src/main/java/com/stumeet/server/studymember/domain/exception/AlreadyStudyJoinMemberException.java @@ -0,0 +1,13 @@ +package com.stumeet.server.studymember.domain.exception; + +import com.stumeet.server.common.exception.model.InvalidStateException; +import com.stumeet.server.common.response.ErrorCode; + +import java.text.MessageFormat; + +public class AlreadyStudyJoinMemberException extends InvalidStateException { + private static final String MESSAGE = "이미 스터디에 가입한 멤버입니다. studyId={0}, memberId={1}"; + public AlreadyStudyJoinMemberException(Long studyId, Long memberId) { + super(MessageFormat.format(MESSAGE, studyId, memberId), ErrorCode.ALREADY_STUDY_JOIN_MEMBER_EXCEPTION); + } +} diff --git a/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java b/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java index 3d30ec64..7955cda7 100644 --- a/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java +++ b/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java @@ -3,9 +3,13 @@ import com.stumeet.server.common.exception.model.InvalidStateException; import com.stumeet.server.common.response.ErrorCode; +import java.text.MessageFormat; + public class NotStudyAdminException extends InvalidStateException { - public NotStudyAdminException(String message) { - super(message, ErrorCode.NOT_STUDY_ADMIN_EXCEPTION); + + private static final String MESSAGE = "스터디 관리자가 아닙니다. 전달받은 studyId={0}, memberId={1}"; + public NotStudyAdminException(Long studyId, Long memberId) { + super(MessageFormat.format(MESSAGE, studyId, memberId), ErrorCode.NOT_STUDY_ADMIN_EXCEPTION); } } diff --git a/src/main/java/com/stumeet/server/studymember/domain/exception/StudyMemberNotJoinedException.java b/src/main/java/com/stumeet/server/studymember/domain/exception/StudyMemberNotJoinedException.java index 0d040e51..c90bb9cc 100644 --- a/src/main/java/com/stumeet/server/studymember/domain/exception/StudyMemberNotJoinedException.java +++ b/src/main/java/com/stumeet/server/studymember/domain/exception/StudyMemberNotJoinedException.java @@ -3,9 +3,11 @@ import com.stumeet.server.common.exception.model.InvalidStateException; import com.stumeet.server.common.response.ErrorCode; -public class StudyMemberNotJoinedException extends InvalidStateException { +import java.text.MessageFormat; - public StudyMemberNotJoinedException(String message) { - super(message, ErrorCode.STUDY_MEMBER_NOT_JOINED_EXCEPTION); +public class StudyMemberNotJoinedException extends InvalidStateException { + private static final String MESSAGE = "스터디에 가입된 멤버가 아닙니다. 전달받은 studyId={0}, memberId={1}"; + public StudyMemberNotJoinedException(Long studyId, Long memberId) { + super(MessageFormat.format(MESSAGE, studyId, memberId), ErrorCode.STUDY_MEMBER_NOT_JOINED_EXCEPTION); } } diff --git a/src/test/java/com/stumeet/server/stub/StudyMemberStub.java b/src/test/java/com/stumeet/server/stub/StudyMemberStub.java index c0dd5ceb..4fcd141a 100644 --- a/src/test/java/com/stumeet/server/stub/StudyMemberStub.java +++ b/src/test/java/com/stumeet/server/stub/StudyMemberStub.java @@ -14,6 +14,14 @@ private StudyMemberStub() { } public static StudyMemberJoinCommand getJoinCommand() { + return StudyMemberJoinCommand.builder() + .studyId(1L) + .memberId(2L) + .isAdmin(true) + .build(); + } + + public static StudyMemberJoinCommand getAlreadyJoinCommand() { return StudyMemberJoinCommand.builder() .studyId(1L) .memberId(1L) diff --git a/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberJoinApiTest.java b/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberJoinApiTest.java new file mode 100644 index 00000000..7582fe5d --- /dev/null +++ b/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberJoinApiTest.java @@ -0,0 +1,140 @@ +package com.stumeet.server.studymember.adapter.in.web; + +import com.stumeet.server.common.auth.model.AuthenticationHeader; +import com.stumeet.server.common.response.ErrorCode; +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.post; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class StudyMemberJoinApiTest extends ApiTest { + + @Nested + @DisplayName("스터디 가입 API") + class Join { + + private static final String PATH = "/api/v1/studies/{studyId}/members"; + + @Test + @WithMockMember(id = 2L) + @DisplayName("[성공] 스터디 가입에 성공한다.") + void successTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()) + .andDo(document("study-member-join/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember(id = 0L) + @DisplayName("[실패] 존재하지 않는 멤버인 경우 예외가 발생한다.") + void failByNotExistMemberTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ErrorCode.MEMBER_NOT_FOUND.getHttpStatusCode())) + .andExpect(jsonPath("$.message").value(ErrorCode.MEMBER_NOT_FOUND.getMessage())) + .andDo(document("study-member-join/fail/not-exist-member", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 존재하지 않는 스터디인 경우 예외가 발생한다.") + void failByNotExistStudyTest() throws Exception { + Long studyId = StudyStub.getInvalidStudyId(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ErrorCode.STUDY_NOT_FOUND.getHttpStatusCode())) + .andExpect(jsonPath("$.message").value(ErrorCode.STUDY_NOT_FOUND.getMessage())) + .andDo(document("study-member-join/fail/not-exist-study", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 스터디에 이미 가입한 멤버인 경우 예외가 발생한다.") + void failByAlreadyJoinMemberTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(ErrorCode.ALREADY_STUDY_JOIN_MEMBER_EXCEPTION.getHttpStatusCode())) + .andExpect(jsonPath("$.message").value(ErrorCode.ALREADY_STUDY_JOIN_MEMBER_EXCEPTION.getMessage())) + .andDo(document("study-member-join/fail/already-join-member", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + 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/StudyMemberJoinServiceTest.java b/src/test/java/com/stumeet/server/studymember/application/service/StudyMemberJoinServiceTest.java index f783aa84..f8b7c040 100644 --- a/src/test/java/com/stumeet/server/studymember/application/service/StudyMemberJoinServiceTest.java +++ b/src/test/java/com/stumeet/server/studymember/application/service/StudyMemberJoinServiceTest.java @@ -5,9 +5,11 @@ import com.stumeet.server.stub.StudyMemberStub; 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.in.command.StudyMemberJoinCommand; import com.stumeet.server.studymember.application.port.in.mapper.StudyMemberUseCaseMapper; import com.stumeet.server.studymember.application.port.out.StudyMemberJoinPort; +import com.stumeet.server.studymember.domain.exception.AlreadyStudyJoinMemberException; import com.stumeet.server.template.UnitTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -37,6 +39,9 @@ class StudyMemberJoinServiceTest extends UnitTest { @Mock private StudyMemberJoinPort studyMemberJoinPort; + @Mock + private StudyMemberValidationUseCase studyMemberValidationUseCase; + @Nested @DisplayName("[단위테스트] 스터디 가입") class Join { @@ -55,6 +60,7 @@ void successTest() { private void verifyMethodCall() { then(memberValidationUseCase).should().checkById(any()); then(studyValidationUseCase).should().checkById(any()); + then(studyMemberValidationUseCase).should().checkAlreadyStudyJoinMember(any(), any()); then(studyMemberUseCaseMapper).should().toDomain(any()); then(studyMemberJoinPort).should().join(any()); } @@ -78,9 +84,21 @@ void notExistsStudyTest() { StudyMemberJoinCommand command = StudyMemberStub.getJoinCommand(); willThrow(StudyNotExistsException.class) - .given(studyValidationUseCase).checkById(command.memberId()); + .given(studyValidationUseCase).checkById(command.studyId()); assertThatCode(() -> studyMemberJoinService.join(command)) .isInstanceOf(StudyNotExistsException.class);} } + + @Test + @DisplayName("[실패] 이미 가입한 멤버가 가입을 시도할 경우 예외 발생") + void alreadyJoinTest() { + StudyMemberJoinCommand command = StudyMemberStub.getAlreadyJoinCommand(); + + willThrow(AlreadyStudyJoinMemberException.class) + .given(studyMemberValidationUseCase).checkAlreadyStudyJoinMember(command.studyId(), command.memberId()); + + assertThatCode(() -> studyMemberJoinService.join(command)) + .isInstanceOf(AlreadyStudyJoinMemberException.class); + } } \ No newline at end of file diff --git a/src/test/resources/db/setup.sql b/src/test/resources/db/setup.sql index 1f2cc760..930b9e67 100644 --- a/src/test/resources/db/setup.sql +++ b/src/test/resources/db/setup.sql @@ -12,7 +12,11 @@ INSERT INTO study_tag (study_domain_id, name) VALUES (1, '객체지향프로그 INSERT INTO member (id, name, image, region, profession_id, role, auth_type, tier, experience, is_deleted, deleted_at) VALUES (1, 'test1', 'http://localhost:4572/user/1/profile/2024030416531039839905-b7e8-4ad3-9552-7d9cbc01cb14-test.jpg', - '서울', 1, 'FIRST_LOGIN', 'OAUTH', 'SEED', 0.0, false, null); + '서울', 1, 'MEMBER', 'OAUTH', 'SEED', 0.0, false, null); + +INSERT INTO member (id, name, image, region, profession_id, role, auth_type, tier, experience, is_deleted, deleted_at) +VALUES (2, 'test2', 'http://localhost:4572/user/1/profile/2024030416531039839905-b7e8-4ad3-9552-7d9cbc01cb14-test.jpg', + '서울', 1, 'MEMBER', 'OAUTH', 'SEED', 0.0, false, null); INSERT INTO study_member(member_id, study_id, is_admin, is_sent_grape) VALUES (1, 1, true, false); \ No newline at end of file