diff --git a/backend/src/main/java/com/yigongil/backend/domain/round/Round.java b/backend/src/main/java/com/yigongil/backend/domain/round/Round.java index 79795fb48..14ce663ba 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/round/Round.java +++ b/backend/src/main/java/com/yigongil/backend/domain/round/Round.java @@ -56,7 +56,7 @@ public class Round extends BaseEntity { @Cascade(CascadeType.PERSIST) @OnDelete(action = OnDeleteAction.CASCADE) - @OneToMany + @OneToMany(orphanRemoval = true) @JoinColumn(name = "round_id", nullable = false) private List roundOfMembers = new ArrayList<>(); diff --git a/backend/src/main/java/com/yigongil/backend/domain/roundofmember/RoundOfMember.java b/backend/src/main/java/com/yigongil/backend/domain/roundofmember/RoundOfMember.java index 290cb1ef0..9e0fd0c75 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/roundofmember/RoundOfMember.java +++ b/backend/src/main/java/com/yigongil/backend/domain/roundofmember/RoundOfMember.java @@ -4,6 +4,7 @@ import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.study.Study; import java.util.List; +import java.util.Objects; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -59,4 +60,23 @@ public void completeRound() { public boolean isMemberEquals(Member member) { return this.member.equals(member); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RoundOfMember roundOfMember)) { + return false; + } + if (id == null || roundOfMember.getId() == null) { + return false; + } + return id.equals(roundOfMember.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java index 6c10593bb..2956fc87c 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java @@ -413,8 +413,19 @@ private boolean isAlreadyExist(Member member) { } public void exit(Member member) { + validateCanExitMemberSize(); rounds.forEach(round -> round.exit(member)); - findStudyMemberBy(member).failStudy(); + findStudyMemberBy(member).exit(); + } + + private void validateCanExitMemberSize() { + long studyMemberCount = studyMembers.stream() + .filter(StudyMember::isStudyMember) + .count(); + if (studyMemberCount <= MIN_MEMBER_SIZE) { + throw new InvalidMemberSizeException("스터디 멤버 수가 2명 이하일 때는 탈퇴할 수 없습니다.", + (int) studyMemberCount); + } } private StudyMember findStudyMemberBy(Member member) { diff --git a/backend/src/main/java/com/yigongil/backend/domain/studymember/Role.java b/backend/src/main/java/com/yigongil/backend/domain/studymember/Role.java index 33051f4df..af9b1cf88 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/studymember/Role.java +++ b/backend/src/main/java/com/yigongil/backend/domain/studymember/Role.java @@ -10,6 +10,7 @@ public enum Role { STUDY_MEMBER(1), APPLICANT(2), NO_ROLE(3), + EXIT(4), ; private final int code; diff --git a/backend/src/main/java/com/yigongil/backend/domain/studymember/StudyMember.java b/backend/src/main/java/com/yigongil/backend/domain/studymember/StudyMember.java index 9ede7f9c5..47cc203c4 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/studymember/StudyMember.java +++ b/backend/src/main/java/com/yigongil/backend/domain/studymember/StudyMember.java @@ -85,6 +85,10 @@ public boolean isMaster() { return this.role == Role.MASTER; } + public boolean isExit() { + return this.role == Role.EXIT; + } + public void completeSuccessfully() { int successfulRoundCount = study.calculateSuccessfulRoundCount(member); int defaultRoundExperience = EXPERIENCE_BASE_UNIT * 2; @@ -94,7 +98,8 @@ public void completeSuccessfully() { this.studyResult = StudyResult.SUCCESS; } - public void failStudy() { + public void exit() { + this.role = Role.EXIT; this.studyResult = StudyResult.FAIL; } diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java index 93c31490e..342023a58 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java @@ -15,7 +15,6 @@ import com.yigongil.backend.response.RoundResponse; import com.yigongil.backend.response.StudyDetailResponse; import com.yigongil.backend.response.StudyListItemResponse; -import com.yigongil.backend.response.StudyMemberResponse; import com.yigongil.backend.response.UpcomingStudyResponse; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; @@ -451,24 +450,15 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext) { .when() .delete("/studies/{studyId}/exit", studyId) .then().log().all(); - - ExtractableResponse response = given().log().all() - .header(HttpHeaders.AUTHORIZATION, token) - .when() - .get("/studies/{studyId}/", studyId) - .then().log().all() - .extract(); - - sharedContext.setResponse(response); } @Then("{string} 이 {string} 스터디에 참여하지 않는다.") public void 스터디에_참여하지_않는다(String githubId, String studyName) { Long id = sharedContext.getId(githubId); - StudyDetailResponse response = sharedContext.getResponse().as(StudyDetailResponse.class); + MembersCertificationResponse response = sharedContext.getResponse().as(MembersCertificationResponse.class); - assertThat(response.members()).map(StudyMemberResponse::id).doesNotContain(id); + assertThat(response.others()).map(MemberCertificationResponse::id).doesNotContain(id); } @Then("{string}는 {string} 스터디 구성원에 포함되어 있지않다.") diff --git a/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java b/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java index 9001a9230..e8eb326bc 100644 --- a/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java +++ b/backend/src/test/java/com/yigongil/backend/domain/round/RoundTest.java @@ -1,5 +1,8 @@ package com.yigongil.backend.domain.round; +import static com.yigongil.backend.fixture.MemberFixture.폰노이만; +import static com.yigongil.backend.fixture.RoundOfMemberFixture.김진우_라오멤; +import static com.yigongil.backend.fixture.RoundOfMemberFixture.노이만_라오멤; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -7,7 +10,6 @@ import com.yigongil.backend.exception.InvalidTodoLengthException; import com.yigongil.backend.exception.NotStudyMasterException; import com.yigongil.backend.fixture.RoundFixture; -import com.yigongil.backend.fixture.RoundOfMemberFixture; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -80,9 +82,9 @@ class 머스트두_진행률_계산 { void 멤버들의_진행률을_계산한다() { //given Round round = RoundFixture.아이디없는_라운드.toRoundWithRoundOfMember( - RoundOfMemberFixture.노이만_라오멤, - RoundOfMemberFixture.노이만_라오멤, - RoundOfMemberFixture.노이만_라오멤 + 노이만_라오멤, + 노이만_라오멤, + 노이만_라오멤 ); round.getRoundOfMembers().get(0).completeRound(); @@ -97,9 +99,9 @@ class 머스트두_진행률_계산 { void 머스트두를_완료한_멤버가_없다() { //given Round round = RoundFixture.아이디없는_라운드.toRoundWithRoundOfMember( - RoundOfMemberFixture.노이만_라오멤, - RoundOfMemberFixture.노이만_라오멤, - RoundOfMemberFixture.노이만_라오멤 + 노이만_라오멤, + 노이만_라오멤, + 노이만_라오멤 ); //when @@ -109,4 +111,24 @@ class 머스트두_진행률_계산 { assertThat(result).isZero(); } } + + @Nested + class 라운드_나가기 { + @Test + void 라운드를_나가면_RoundOfMember에서_사라진다() { + // given + Round round = RoundFixture.아이디없는_라운드.toRoundWithRoundOfMember( + 김진우_라오멤, + 노이만_라오멤 + ); + + // when + round.exit(폰노이만.toMember()); + boolean roundOfMemberExists = round.getRoundOfMembers().stream() + .anyMatch(roundOfMember -> roundOfMember.equals(노이만_라오멤)); + + // then + assertThat(roundOfMemberExists).isFalse(); + } + } } diff --git a/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java b/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java index c41b72e1d..f1b19c028 100644 --- a/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java +++ b/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java @@ -2,9 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.round.Round; +import com.yigongil.backend.domain.studymember.StudyMember; import com.yigongil.backend.exception.InvalidMemberSizeException; import com.yigongil.backend.exception.InvalidProcessingStatusException; import com.yigongil.backend.fixture.MemberFixture; @@ -169,4 +171,50 @@ void setUp() { assertThatThrownBy(() -> study.apply(member3)) .isInstanceOf(InvalidMemberSizeException.class); } + + @Nested + class 스터디_탈퇴 { + @Test + void 스터디_탈퇴() { + // given + Study study = StudyFixture.자바_스터디_모집중.toStudy(); + Member master = MemberFixture.김진우.toMember(); + Member member1 = MemberFixture.마틴파울러.toMember(); + study.apply(member1); + study.permit(member1, master); + + Member member2 = MemberFixture.폰노이만.toMember(); + study.apply(member2); + study.permit(member2, master); + + StudyMember studyMember1 = study.getStudyMembers().stream() + .filter(studyMember -> studyMember.getMember() + .equals(member1)) + .findAny() + .get(); + // when + study.exit(member1); + + // then + assertAll( + () -> assertThat(study.sizeOfCurrentMembers()).isEqualTo(2), + () -> assertThat(studyMember1.isExit()).isTrue() + ); + + } + + @Test + void 멤버의_수가_2명_이하면_예외가_발생한다() { + // given + Study study = StudyFixture.자바_스터디_모집중_정원_2.toStudy(); + Member master = MemberFixture.김진우.toMember(); + Member member = MemberFixture.마틴파울러.toMember(); + study.apply(member); + study.permit(member, master); + + // when, then + assertThatThrownBy(() -> study.exit(member)) + .isInstanceOf(InvalidMemberSizeException.class); + } + } } diff --git a/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java b/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java index c892b6249..f521306f6 100644 --- a/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java +++ b/backend/src/test/java/com/yigongil/backend/fixture/RoundFixture.java @@ -35,7 +35,7 @@ public Round toRound() { .id(id) .mustDo(content) .master(master) - .roundOfMembers(new ArrayList<>(List.of(RoundOfMemberFixture.김진우_라운드_삼.toRoundOfMember(), RoundOfMemberFixture.노이만_라오멤.toRoundOfMember()))) + .roundOfMembers(new ArrayList<>(List.of(RoundOfMemberFixture.김진우_라오멤.toRoundOfMember(), RoundOfMemberFixture.노이만_라오멤.toRoundOfMember()))) .build(); } @@ -43,11 +43,12 @@ public Round toRoundWithRoundOfMember(RoundOfMemberFixture... roundOfMemberFixtu List roundOfMembers = Arrays.stream(roundOfMemberFixtures) .map(RoundOfMemberFixture::toRoundOfMember) .toList(); + return Round.builder() .id(id) .mustDo(content) .master(master) - .roundOfMembers(roundOfMembers) + .roundOfMembers(new ArrayList<>(roundOfMembers)) .build(); } } diff --git a/backend/src/test/java/com/yigongil/backend/fixture/RoundOfMemberFixture.java b/backend/src/test/java/com/yigongil/backend/fixture/RoundOfMemberFixture.java index c44b5bda4..4795574f9 100644 --- a/backend/src/test/java/com/yigongil/backend/fixture/RoundOfMemberFixture.java +++ b/backend/src/test/java/com/yigongil/backend/fixture/RoundOfMemberFixture.java @@ -5,7 +5,7 @@ public enum RoundOfMemberFixture { - 김진우_라운드_삼(1L, MemberFixture.김진우.toMember(), false), + 김진우_라오멤(2L, MemberFixture.김진우.toMember(), false), 노이만_라오멤(1L, MemberFixture.폰노이만.toMember(), false), ; diff --git a/backend/src/test/resources/features/study-exit.feature b/backend/src/test/resources/features/study-exit.feature new file mode 100644 index 000000000..eaf5b1f41 --- /dev/null +++ b/backend/src/test/resources/features/study-exit.feature @@ -0,0 +1,19 @@ +Feature: 스터디를 탈퇴한다. + + Scenario: 스터디를 탈퇴한다. + Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. + Given "jinwoo"가 제목-"자바1", 정원-"6"명, 최소 주차-"1"주, 주당 진행 횟수-"3"회, 소개-"스터디소개1"로 스터디를 개설한다. + + Given "noiman"의 깃허브 아이디로 회원가입을 한다. + Given 깃허브 아이디가 "noiman"인 멤버가 이름이 "자바1"스터디에 신청한다. + Given "jinwoo"가 "noiman"의 "자바1" 스터디 신청을 수락한다. + + Given "yujamint"의 깃허브 아이디로 회원가입을 한다. + Given 깃허브 아이디가 "yujamint"인 멤버가 이름이 "자바1"스터디에 신청한다. + Given "jinwoo"가 "yujamint"의 "자바1" 스터디 신청을 수락한다. + + Given "jinwoo"가 이름이 "자바1"인 스터디를 "MONDAY"에 진행되도록 하여 시작한다. + + When "noiman" 이 "자바1" 스터디에서 탈퇴한다. + When "jinwoo"가 "자바1" 스터디의 인증 목록을 조회한다. + Then "noiman" 이 "자바1" 스터디에 참여하지 않는다. diff --git a/backend/src/test/resources/features/study-progress.feature b/backend/src/test/resources/features/study-progress.feature index 060ecbb9e..186e8dc3e 100644 --- a/backend/src/test/resources/features/study-progress.feature +++ b/backend/src/test/resources/features/study-progress.feature @@ -60,16 +60,3 @@ Feature: 스터디를 진행한다 When "noiman"가 마이페이지를 조회한다. Then 조회한 멤버의 경험치가 상승했다. - - Scenario: 스터디를 탈퇴한다. - - Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. - Given "jinwoo"가 제목-"자바1", 정원-"6"명, 최소 주차-"1"주, 주당 진행 횟수-"3"회, 소개-"스터디소개1"로 스터디를 개설한다. - Given "noiman"의 깃허브 아이디로 회원가입을 한다. - Given 깃허브 아이디가 "noiman"인 멤버가 이름이 "자바1"스터디에 신청한다. - Given "jinwoo"가 "noiman"의 "자바1" 스터디 신청을 수락한다. - Given "jinwoo"가 이름이 "자바1"인 스터디를 "MONDAY"에 진행되도록 하여 시작한다. - - When "noiman" 이 "자바1" 스터디에서 탈퇴한다. - - Then "noiman" 이 "자바1" 스터디에 참여하지 않는다.