Skip to content

Commit

Permalink
Feature/#242 팀원 삭제 후 재초대 시 팀원 조회에서 발생하는 서버 에러 해결 (#243)
Browse files Browse the repository at this point in the history
* fix: 팀원 삭제 시 teamRole도 삭제하는 로직 추가

* test: 팀원 삭제 후 재초대 시 팀원 조회가 불가한 상황에 대한 테스트 추가

* fix: 팀장 삭제 시 팀장 본인은 삭제 불가하도록 변경

* fix: 참여자 삭제 시 참여자 본인은 삭제하지 못하도록 변경

* fix: 스터디장으로 임명된 팀원은 삭제 불가하도록 변경, 팀원 삭제 시 스터디원 & 권한도 모두 삭제

* fix: 참여자(스터디원) 삭제는 스터디장과 팀장이 가능하도록 변경
- 참여자 삭제 시 권한도 삭제되도록 변경
- 참여자 탈퇴 시 권한도 삭제되도록 변경

* feat: 팀장이 스터디장을 삭제하는 경우 팀장이 스터디장 권한을 위임받는 것으로 수정
- 팀장이 스터디장을 삭제하는 경우 스터디장 부재가 되므로 스터디를 정상적으로 운영할 수 없게 되는 현상 방지

* fix: 예외처리 이름이 동일하여 의미가 잘 전달되지 않던 부분 수정

* fix: 스터디 구성원이 아니거나 팀 구성원이 아니면 예외 처리를 하던 것을 false를 반환하는 것으로 변경

* chore: 권한 로직을 먼저 수행하도록 변경

* test: 추가로 개발된 사항에 대한 테스트 개발
  • Loading branch information
JJimini authored Nov 30, 2024
1 parent c5b7af4 commit 4de5721
Show file tree
Hide file tree
Showing 20 changed files with 370 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package doore.member.application;

import static doore.member.exception.MemberTeamExceptionType.CANNOT_DELETE_TEAM_LEADER_SELF;

import doore.member.application.convenience.StudyRoleConvenience;
import doore.member.application.convenience.TeamRoleConvenience;
import doore.member.application.convenience.TeamRoleValidateAccessPermission;
import doore.member.domain.repository.MemberTeamRepository;
import doore.member.exception.MemberTeamException;
import doore.study.application.convenience.ParticipantConvenience;
import doore.study.application.convenience.StudyConvenience;
import doore.study.domain.Study;
import doore.team.application.convenience.TeamValidateAccessPermission;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -16,10 +25,34 @@ public class MemberTeamCommandService {
private final TeamRoleValidateAccessPermission teamRoleValidateAccessPermission;
private final TeamValidateAccessPermission teamValidateAccessPermission;

private final TeamRoleConvenience teamRoleConvenience;
private final StudyConvenience studyConvenience;
private final StudyRoleConvenience studyRoleConvenience;
private final ParticipantConvenience participantConvenience;

public void deleteMemberTeam(final Long teamId, final Long deleteMemberId, final Long teamLeaderId) {
teamValidateAccessPermission.validateExistTeam(teamId);
checkIsEqualDeleteMemberIdAndTeamLeaderId(deleteMemberId, teamLeaderId);
teamRoleValidateAccessPermission.validateExistTeamLeader(teamId, teamLeaderId);
teamRoleValidateAccessPermission.validateExistMemberTeam(teamId, deleteMemberId);
deleteStudiesAndParticipants(teamId, deleteMemberId);
memberTeamRepository.deleteByTeamIdAndMemberId(teamId, deleteMemberId);
teamRoleConvenience.deleteByTeamIdAndMemberId(teamId, deleteMemberId);
}

private void checkIsEqualDeleteMemberIdAndTeamLeaderId(final Long deleteMemberId, final Long teamLeaderId) {
if (deleteMemberId.equals(teamLeaderId)) {
throw new MemberTeamException(CANNOT_DELETE_TEAM_LEADER_SELF);
}
}

private void deleteStudiesAndParticipants(final Long teamId, final Long deleteMemberId) {
List<Study> studies = studyConvenience.findAllByTeamIdAndMemberId(teamId, deleteMemberId);
studyRoleConvenience.isStudyLeader(studies, deleteMemberId);

studies.forEach(study -> {
participantConvenience.deleteByParticipantIdAndMemberId(study.getId(), deleteMemberId);
studyRoleConvenience.deleteByStudyIdAndMemberId(study.getId(), deleteMemberId);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package doore.member.application.convenience;

import static doore.member.domain.StudyRoleType.*;
import static doore.member.domain.StudyRoleType.ROLE_스터디원;
import static doore.member.domain.StudyRoleType.ROLE_스터디장;
import static doore.member.exception.MemberExceptionType.CANNOT_DELETE_STUDY_LEADER;
import static doore.member.exception.MemberExceptionType.NOT_FOUND_MEMBER_ROLE_IN_STUDY;

import doore.member.domain.StudyRole;
import doore.member.domain.StudyRoleType;
import doore.member.domain.repository.StudyRoleRepository;
import doore.member.exception.MemberException;
import doore.study.domain.Study;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -44,4 +48,17 @@ public StudyRoleType findStudyRoleType(Long studyId, Long memberId) {
public Long findStudyLeaderId(final Long studyId) {
return studyRoleRepository.findLeaderIdByStudyId(studyId);
}

public void isStudyLeader(final List<Study> studies, final Long memberId) {
boolean isStudyLeader = studies.stream().anyMatch(
study -> studyRoleRepository.existsByStudyIdAndMemberIdAndStudyRoleType(study.getId(), memberId,
ROLE_스터디장));
if (isStudyLeader) {
throw new MemberException(CANNOT_DELETE_STUDY_LEADER);
}
}

public void deleteByStudyIdAndMemberId(final Long studyId, final Long memberId) {
studyRoleRepository.deleteByStudyIdAndMemberId(studyId, memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ public StudyRole getValidateExistParticipant(final Long studyId, final Long memb
return studyRoleRepository.findStudyRoleByStudyIdAndMemberId(studyId, memberId)
.orElseThrow(() -> new MemberException(UNAUTHORIZED));
}

public boolean isStudyLeader(final Long studyId, final Long memberId) {
return studyRoleRepository.findStudyRoleByStudyIdAndMemberId(studyId, memberId)
.map(studyRole -> ROLE_스터디장.equals(studyRole.getStudyRoleType()))
.orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ public void assignTeamMemberRole(final Long teamId, final Long memberId) {
.memberId(memberId)
.build());
}

public void deleteByTeamIdAndMemberId(final Long teamId, final Long memberId) {
teamRoleRepository.deleteByTeamIdAndMemberId(teamId, memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ public TeamRole getValidateExistMemberTeam(final Long teamId, final Long memberI
return teamRoleRepository.findTeamRoleByTeamIdAndMemberId(teamId, memberId)
.orElseThrow(() -> new MemberException(UNAUTHORIZED));
}

public boolean isTeamLeader(final Long teamId, final Long memberId) {
return teamRoleRepository.findTeamRoleByTeamIdAndMemberId(teamId, memberId)
.map(teamRole -> ROLE_팀장.equals(teamRole.getTeamRoleType()))
.orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface ParticipantRepository extends JpaRepository<Participant, Long>
List<Participant> findAllByStudyIdAndIsDeletedFalse(Long studyId);

List<Participant> findByMemberId(Long memberId);

void deleteByStudyIdAndMemberId(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface StudyRoleRepository extends JpaRepository<StudyRole, Long> {
Optional<StudyRole> findStudyRoleByStudyIdAndStudyRoleType(Long studyId, StudyRoleType studyRoleType);
@Query("SELECT sr.memberId FROM StudyRole sr WHERE sr.studyId = :studyId AND sr.studyRoleType = 'ROLE_스터디장'")
Long findLeaderIdByStudyId(Long studyId);
boolean existsByStudyIdAndMemberIdAndStudyRoleType(Long studyId, Long memberId, StudyRoleType studyRoleType);
void deleteByStudyIdAndMemberId(final Long studyId, final Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public interface TeamRoleRepository extends JpaRepository<TeamRole, Long> {
@Query("SELECT tr.memberId FROM TeamRole tr WHERE tr.teamId = :teamId AND tr.teamRoleType = 'ROLE_팀장'")
Long findLeaderIdByTeamId(Long teamId);
List<TeamRole> findAllByTeamId(final Long teamId);
void deleteByTeamIdAndMemberId(final Long teamId, final Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum MemberExceptionType implements BaseExceptionType {
NOT_FOUND_MEMBER_IN_STUDY(HttpStatus.NOT_FOUND, "스터디 내 해당 회원을 찾을 수 없습니다."),
ALREADY_JOIN_TEAM_MEMBER(HttpStatus.BAD_REQUEST, "이미 가입된 팀원입니다."),
ALREADY_JOIN_STUDY_MEMBER(HttpStatus.BAD_REQUEST, "이미 가입된 스터디원입니다."),
CANNOT_DELETE_STUDY_LEADER(HttpStatus.BAD_REQUEST, "스터디장을 맡고 있다면 팀원 삭제가 불가능합니다."),
;

private final HttpStatus httpStatus;
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/doore/member/exception/MemberTeamException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package doore.member.exception;

import doore.base.BaseException;
import doore.base.BaseExceptionType;

public class MemberTeamException extends BaseException {
private final MemberTeamExceptionType exceptionType;

public MemberTeamException(final MemberTeamExceptionType exceptionType) {
super(exceptionType.errorMessage());
this.exceptionType = exceptionType;
}

@Override
public BaseExceptionType exceptionType() {
return exceptionType;
}
}
28 changes: 28 additions & 0 deletions src/main/java/doore/member/exception/MemberTeamExceptionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package doore.member.exception;

import doore.base.BaseExceptionType;
import org.springframework.http.HttpStatus;

public enum MemberTeamExceptionType implements BaseExceptionType {

CANNOT_DELETE_TEAM_LEADER_SELF(HttpStatus.BAD_REQUEST, "팀장 본인은 팀을 탈퇴할 수 없습니다."),
;

private final HttpStatus httpStatus;
private final String errorMessage;

MemberTeamExceptionType(final HttpStatus httpStatus, final String errorMessage) {
this.httpStatus = httpStatus;
this.errorMessage = errorMessage;
}

@Override
public HttpStatus httpStatus() {
return httpStatus;
}

@Override
public String errorMessage() {
return errorMessage;
}
}
19 changes: 19 additions & 0 deletions src/main/java/doore/member/exception/ParticipantException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package doore.member.exception;

import doore.base.BaseException;
import doore.base.BaseExceptionType;

public class ParticipantException extends BaseException {

private final ParticipantExceptionType exceptionType;

public ParticipantException(final ParticipantExceptionType exceptionType) {
super(exceptionType.errorMessage());
this.exceptionType = exceptionType;
}

@Override
public BaseExceptionType exceptionType() {
return exceptionType;
}
}
28 changes: 28 additions & 0 deletions src/main/java/doore/member/exception/ParticipantExceptionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package doore.member.exception;

import doore.base.BaseExceptionType;
import org.springframework.http.HttpStatus;

public enum ParticipantExceptionType implements BaseExceptionType {

CANNOT_DELETE_STUDY_LEADER_SELF(HttpStatus.BAD_REQUEST, "스터디장 본인은 팀을 탈퇴할 수 없습니다."),
;

private final HttpStatus httpStatus;
private final String errorMessage;

ParticipantExceptionType(final HttpStatus httpStatus, final String errorMessage) {
this.httpStatus = httpStatus;
this.errorMessage = errorMessage;
}

@Override
public HttpStatus httpStatus() {
return httpStatus;
}

@Override
public String errorMessage() {
return errorMessage;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package doore.study.application;

import static doore.member.exception.MemberExceptionType.UNAUTHORIZED;
import static doore.member.exception.ParticipantExceptionType.CANNOT_DELETE_STUDY_LEADER_SELF;

import doore.member.application.convenience.MemberValidateAccessPermission;
import doore.member.application.convenience.StudyRoleConvenience;
import doore.member.application.convenience.StudyRoleValidateAccessPermission;
import doore.member.application.convenience.TeamRoleValidateAccessPermission;
import doore.member.domain.Member;
import doore.member.domain.repository.ParticipantRepository;
import doore.member.exception.MemberException;
import doore.member.exception.ParticipantException;
import doore.study.application.convenience.ParticipantConvenience;
import doore.study.application.convenience.StudyConvenience;
import doore.study.application.convenience.StudyValidateAccessPermission;
import doore.study.domain.Study;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -19,8 +27,10 @@ public class ParticipantCommandService {

private final StudyRoleConvenience studyRoleConvenience;
private final ParticipantConvenience participantConvenience;
private final StudyConvenience studyConvenience;

private final StudyRoleValidateAccessPermission studyRoleValidateAccessPermission;
private final TeamRoleValidateAccessPermission teamRoleValidateAccessPermission;
private final MemberValidateAccessPermission memberValidateAccessPermission;
private final StudyValidateAccessPermission studyValidateAccessPermission;

Expand All @@ -33,16 +43,43 @@ public void createParticipant(final Long studyId, final Long memberId, final Lon
}

public void deleteParticipant(final Long studyId, final Long memberId, final Long studyLeaderId) {
studyRoleValidateAccessPermission.validateExistStudyLeader(studyId, studyLeaderId);
studyValidateAccessPermission.validateExistStudy(studyId);
checkStudyLeaderOrTeamLeader(studyId, memberId, studyLeaderId);
checkIsEqualDeleteMemberIdAndStudyLeaderId(memberId, studyLeaderId);
final Member member = memberValidateAccessPermission.getValidateExistMember(memberId);
participantRepository.deleteByStudyIdAndMember(studyId, member);
studyRoleConvenience.deleteByStudyIdAndMemberId(studyId, memberId);
}

public void withdrawParticipant(final Long studyId, final Long memberId, final Long participantId) {
studyRoleValidateAccessPermission.validateExistParticipant(studyId, participantId);
studyValidateAccessPermission.validateExistStudy(studyId);
final Member member = memberValidateAccessPermission.getValidateExistMember(memberId);
participantRepository.deleteByStudyIdAndMember(studyId, member);
studyRoleConvenience.deleteByStudyIdAndMemberId(studyId, memberId);
}

private void checkIsEqualDeleteMemberIdAndStudyLeaderId(final Long deleteMemberId, final Long studyLeaderId) {
if (deleteMemberId.equals(studyLeaderId)) {
throw new ParticipantException(CANNOT_DELETE_STUDY_LEADER_SELF);
}
}

private void checkStudyLeaderOrTeamLeader(final Long studyId, final Long deleteMemberId, final Long leaderId) {
final Study study = studyConvenience.findById(studyId);
final Long teamId = study.getTeamId();
if (!(studyRoleValidateAccessPermission.isStudyLeader(studyId, leaderId)
|| teamRoleValidateAccessPermission.isTeamLeader(teamId, leaderId))) {
throw new MemberException(UNAUTHORIZED);
}
assignStudyLeaderToTeamLeader(studyId, deleteMemberId, teamId, leaderId);
}

private void assignStudyLeaderToTeamLeader(final Long studyId, final Long deleteMemberId, final Long teamId,
final Long leaderId) {
if (studyRoleValidateAccessPermission.isStudyLeader(studyId, deleteMemberId)
&& teamRoleValidateAccessPermission.isTeamLeader(teamId, leaderId)) {
studyRoleConvenience.assignStudyLeaderRole(studyId, leaderId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public void deleteAllParticipant(final Long studyId) {
final List<Participant> participants = participantRepository.findAllByStudyId(studyId);
participantRepository.deleteAll(participants);
}

public void deleteByParticipantIdAndMemberId(final Long participantId, final Long memberId) {
participantRepository.deleteByStudyIdAndMemberId(participantId, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package doore.study.application.convenience;

import static doore.study.exception.StudyExceptionType.NOT_FOUND_STUDY;

import doore.study.domain.Study;
import doore.study.domain.repository.StudyRepository;
import doore.study.exception.StudyException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -16,7 +20,15 @@ public Study findByDocumentId(final Long documentId) {
return studyRepository.findByDocumentId(documentId);
}

public Study findByCurriculumItemId(Long curriculumId) {
public Study findByCurriculumItemId(final Long curriculumId) {
return studyRepository.findByCurriculumItemId(curriculumId);
}

public List<Study> findAllByTeamIdAndMemberId(final Long teamId, final Long memberId) {
return studyRepository.findAllByTeamIdAndMemberId(teamId, memberId);
}

public Study findById(final Long studyId) {
return studyRepository.findById(studyId).orElseThrow(() -> new StudyException(NOT_FOUND_STUDY));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ public interface StudyRepository extends JpaRepository<Study, Long> {
Page<Study> findAllByTeamId(Long teamId, Pageable pageable);
@Query("select s from Study s join Document d on d.groupId = s.id where d.id = :documentId")
Study findByDocumentId(Long documentId);

@Query("SELECT s FROM Study s JOIN Participant p ON p.studyId = s.id " +
"WHERE p.member.id = :memberId AND s.teamId = :teamId")
List<Study> findAllByTeamIdAndMemberId(Long teamId, Long memberId);
}
Loading

0 comments on commit 4de5721

Please sign in to comment.