Skip to content

Commit

Permalink
✨ [STMT-171] 스터디 가입 API 구현 (#114)
Browse files Browse the repository at this point in the history
* ✨ [STMT-171] 스터디 가입 API 구현

* ✅ [STMT-171] 테스트용 유저 데이터 추가

* ✅ [STMT-171] 멤버 스터디 가입 API 테스트 코드 작성 및 문서화

* ✅ [STMT-171] 이미 가입된 멤버일때 실패하는 케이스 추가

* ♻️ [STMT-171] 메시지를 예외 클래스안에서 관리하도록 변경

* ♻️ [STMT-171] first_login 상태에서 MEMBER 상태로 변경
  • Loading branch information
zxcv9203 authored Apr 13, 2024
1 parent 727a95a commit 6a4e449
Show file tree
Hide file tree
Showing 17 changed files with 350 additions and 56 deletions.
29 changes: 29 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
93 changes: 47 additions & 46 deletions src/main/java/com/stumeet/server/common/response/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<Void>> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
8 changes: 8 additions & 0 deletions src/test/java/com/stumeet/server/stub/StudyMemberStub.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 6a4e449

Please sign in to comment.