Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,10 @@ public void deleteCrew(UUID crewId, UUID loginMemberId) {
Crew crew = findCrewById(crewId);
crew.softDelete(loginMemberId);
}
}

@Override
public void expelMember(UUID crewId, CrewExpelRequest request) {
Crew crew = findCrewById(crewId);
crew.getCrewMembers().expel(request.leaderId(), request.memberId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.retrip.crew.application.in.request.crew;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.UUID;

@Schema(description = "크루원 추방 Request")
public record CrewExpelRequest(
@Schema(description = "크루 리더 ID")
UUID leaderId,

@Schema(description = "추방할 크루원 ID")
UUID memberId
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.retrip.crew.application.in.usecase;

import com.retrip.crew.application.in.request.crew.CrewCreateRequest;
import com.retrip.crew.application.in.request.crew.CrewExpelRequest;
import com.retrip.crew.application.in.request.crew.CrewLeaderDelegateRequest;
import com.retrip.crew.application.in.request.crew.CrewUpdateRequest;
import com.retrip.crew.application.in.request.crew.CrewWithdrawalRequest;
Expand All @@ -20,4 +21,6 @@ public interface ManageCrewUseCase {
CrewLeaderDelegateResponse delegateCrewLeader(UUID crewId, CrewLeaderDelegateRequest request);

void deleteCrew(UUID crewId, UUID loginMemberId);
}

void expelMember(UUID crewId, CrewExpelRequest request);
}
14 changes: 13 additions & 1 deletion src/main/java/com/retrip/crew/domain/entity/CrewMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ public class CrewMember extends BaseEntity {
@Column(nullable = false)
private UUID memberId;

@Enumerated(EnumType.STRING)
private CrewMemberStatus status;

public CrewMember(Crew crew, UUID memberId, CrewMemberRole crewMemberRole) {
this.id = UUID.randomUUID();
this.crew = crew;
this.memberId = memberId;
this.crewMemberRole = CrewMemberRole.valueOf(crewMemberRole.name());
this.status = CrewMemberStatus.ACTIVE;
}

public boolean isLeader() {
Expand All @@ -48,4 +52,12 @@ public void changeRole(CrewMemberRole role) {
public boolean isCreatedByMe(UUID postCreatedBy) {
return postCreatedBy == memberId;
}
}

public void expel() {
this.status = CrewMemberStatus.EXPELLED;
}

public boolean isExpelled() {
return this.status == CrewMemberStatus.EXPELLED;
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/retrip/crew/domain/entity/CrewMemberStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.retrip.crew.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum CrewMemberStatus {
ACTIVE("ACTIVE", "활동 중"),
EXPELLED("EXPELLED", "추방됨");

private final String code;
private final String viewName;
}
37 changes: 27 additions & 10 deletions src/main/java/com/retrip/crew/domain/entity/CrewMembers.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.retrip.crew.domain.entity;

import com.retrip.crew.domain.CrewTrip;
import com.retrip.crew.domain.exception.CrewMemberExpelFailedException;
import com.retrip.crew.domain.exception.ImpossibleWithdrawCrewException;
import com.retrip.crew.domain.exception.NotCrewLeaderException;
import com.retrip.crew.domain.exception.common.IllegalStateException;
Expand Down Expand Up @@ -40,7 +41,7 @@ public CrewMember getLeader() {
}

public int getSize() {
return this.values.size();
return (int) this.values.stream().filter(member -> !member.isExpelled()).count();
}

public boolean isLeader(UUID memberId) {
Expand All @@ -49,17 +50,20 @@ public boolean isLeader(UUID memberId) {
}

public void addMember(Demand demand, Crew crew) {
if (isDuplicate(demand)) {
throw new IllegalStateException("사용자는 이미 크루 멤버 입니다.");
}
this.values.stream()
.filter(m -> m.getMemberId().equals(demand.getMemberId()))
.findFirst()
.ifPresent(member -> {
if (member.isExpelled()) {
throw new IllegalStateException("추방된 사용자는 다시 크루에 참여 요청 할 수 없습니다.");
}
throw new IllegalStateException("이미 크루에 참여한 사용자입니다.");
});

CrewMember member = new CrewMember(crew, demand.getMemberId(), CrewMemberRole.PARTICIPANT);
values.add(member);
}

private boolean isDuplicate(Demand demand) {
return this.values.stream()
.anyMatch(m -> m.getMemberId().equals(demand.getMemberId()));
}

public void withdraw(UUID memberId, List<CrewTrip> participatingCrewTrips) {
validatePossibleWithdrawal(memberId, participatingCrewTrips);
Expand Down Expand Up @@ -87,7 +91,7 @@ private void validateParticipatingCrewTrips(List<CrewTrip> crewTrips) {

private CrewMember findMember(UUID memberId) {
return this.values.stream()
.filter(m -> memberId.equals(m.getMemberId()))
.filter(m -> memberId.equals(m.getMemberId()) && !m.isExpelled())
.findFirst()
.orElseThrow(() -> new InvalidValueException("크루 멤버가 아닙니다."));
}
Expand All @@ -107,4 +111,17 @@ private void validatePossibleDelegate(CrewMember leader) {
throw new NotCrewLeaderException();
}
}
}

public void expel(UUID leaderId, UUID memberId) {
if (!isLeader(leaderId)) {
throw new NotCrewLeaderException();
}

if (leaderId.equals(memberId)) {
throw new CrewMemberExpelFailedException("자기 자신을 추방할 수 없습니다.");
}

CrewMember memberToExpel = findMember(memberId);
memberToExpel.expel();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ public void rejectDemand(Demand demand) {
}



private static void throwIfNotPending(Demand find) {
if (find.isNotPending()) {
throw new IllegalDemandStateException("참여 요청의 상태가 대기중이 아닙니다.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.retrip.crew.domain.exception;

import com.retrip.crew.domain.exception.common.BusinessException;
import com.retrip.crew.domain.exception.common.ErrorCode;

public class CrewMemberExpelFailedException extends BusinessException {
private static final ErrorCode errorCode = ErrorCode.CREW_MEMBER_EXPEL_FAIL;

public CrewMemberExpelFailedException(String message) {
super(errorCode, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public enum ErrorCode {
POST_DELETE_FAIL(FORBIDDEN, "Crew-011", "자유 게시글을 삭제할 권한이 없습니다."),
QUESTION_UPDATE_FAIL(FORBIDDEN, "Crew-012", "질문 수정 권한이 없습니다."),
QUESTION_DELETE_FAIL(FORBIDDEN, "Crew-013", "질문 삭제 권한이 없습니다."),
QUESTION_NOT_FOUND(BAD_REQUEST, "Crew-014", "질문을 찾을 수 없습니다.");
QUESTION_NOT_FOUND(BAD_REQUEST, "Crew-014", "질문을 찾을 수 없습니다."),
CREW_MEMBER_EXPEL_FAIL(FORBIDDEN, "Crew-015", "크루원 추방에 실패했습니다.");

;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,14 @@ public ApiResponse<?> deleteCrew(
manageCrewUseCase.deleteCrew(crewId, loginMemberId);
return ApiResponse.noContent();
}

@Schema(description = "크루원 추방")
@PutMapping("/{crewId}/expel")
public ApiResponse<Void> expelMember(
@PathVariable final UUID crewId,
@RequestBody CrewExpelRequest request
) {
manageCrewUseCase.expelMember(crewId, request);
return ApiResponse.noContent();
}
}
32 changes: 26 additions & 6 deletions src/test/java/com/retrip/crew/application/in/CrewServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.retrip.crew.application.in.request.IntroductionDeleteRequest;
import com.retrip.crew.application.in.request.IntroductionUpdateRequest;
import com.retrip.crew.application.in.request.crew.CrewCreateRequest;
import com.retrip.crew.application.in.request.crew.CrewExpelRequest;
import com.retrip.crew.application.in.request.crew.CrewOrder;
import com.retrip.crew.application.in.request.crew.CrewUpdateRequest;
import com.retrip.crew.application.in.request.demand.CreateDemandRequest;
Expand All @@ -20,6 +21,7 @@
import com.retrip.crew.domain.entity.CrewMemberRole;
import com.retrip.crew.domain.entity.Demand;
import com.retrip.crew.domain.entity.Introduction;
import com.retrip.crew.domain.exception.DuplicateDemandException;
import com.retrip.crew.domain.exception.NotCrewLeaderException;
import com.retrip.crew.domain.exception.common.InvalidAccessException;
import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse;
Expand Down Expand Up @@ -104,7 +106,7 @@ class CrewServiceTest extends ServiceTest {
"속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.",
100,
MEMBER_ID,
List.of("질문1")
List.of("속초 맛집 아시나요?")
);
crew.demand(MEMBER_ID);
crew.demand(UUID.randomUUID());
Expand All @@ -113,7 +115,7 @@ class CrewServiceTest extends ServiceTest {
CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID);

assertThatThrownBy(() -> demandService.createDemand(save.getId(), request))
.isExactlyInstanceOf(IllegalStateException.class);
.isExactlyInstanceOf(DuplicateDemandException.class);
}

@Test
Expand All @@ -125,7 +127,7 @@ class CrewServiceTest extends ServiceTest {
"속초 크루원 구함",
"속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.",
5,
List.of("질문1")
List.of("속초 맛집을 아시나요?")
);
requests.forEach(request -> {
CrewCreateResponse response = crewService.createCrew(request);
Expand Down Expand Up @@ -155,7 +157,7 @@ class CrewServiceTest extends ServiceTest {
"속초 크루원 구함",
"속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.",
5,
List.of("질문1")
List.of("속초 맛집을 아시나요?")
);
UUID crewId = crewService.createCrew(request).id();

Expand Down Expand Up @@ -310,7 +312,8 @@ class CrewServiceTest extends ServiceTest {
"속초 크루원 구함",
"속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.",
100,
MEMBER_ID
MEMBER_ID,
createDefaultQuestions()
);
Crew savedCrew = crewRepository.save(crew);

Expand All @@ -329,7 +332,8 @@ class CrewServiceTest extends ServiceTest {
"속초 크루원 구함",
"속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.",
100,
LEADER_ID
LEADER_ID,
createDefaultQuestions()
);
Crew savedCrew = crewRepository.save(crew);
Demand demand = savedCrew.demand(MEMBER_ID);
Expand All @@ -338,4 +342,20 @@ class CrewServiceTest extends ServiceTest {
// when && then
assertThrows(NotCrewLeaderException.class, () -> crewService.deleteCrew(savedCrew.getId(), MEMBER_ID));
}

@Test
void 크루원을_추방한다() {
// given
Crew crew = crewRepository.save(createCrewWithMutableMembers(LEADER_ID));
CrewExpelRequest request = new CrewExpelRequest(LEADER_ID, 홍석_ID);

// when
crewService.expelMember(crew.getId(), request);
Crew result = crewService.findCrewById(crew.getId());

// then
assertThat(result.getCrewMembers().getValues()
.stream().filter(member -> member.getMemberId().equals(홍석_ID))
.findFirst().get().isExpelled()).isTrue();
}
}
38 changes: 38 additions & 0 deletions src/test/java/com/retrip/crew/domain/entity/CrewMembersTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.retrip.crew.domain.entity;

import com.retrip.crew.domain.CrewTrip;
import com.retrip.crew.domain.exception.CrewMemberExpelFailedException;
import com.retrip.crew.domain.exception.ImpossibleWithdrawCrewException;
import com.retrip.crew.domain.exception.NotCrewLeaderException;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

Expand Down Expand Up @@ -149,4 +151,40 @@ class CrewMembersTest {
assertThat(newLeader.getCrewMemberRole()).isEqualTo(CrewMemberRole.LEADER);
assertThat(leader.getCrewMemberRole()).isEqualTo(CrewMemberRole.PARTICIPANT);
}

@Test
void 리더가_크루원을_추방한다() {
// given
Crew crew = createCrewWithMutableMembers(LEADER_ID);

// when
crew.getCrewMembers().expel(LEADER_ID, 홍석_ID);

// then
CrewMember expelledMember = crew.getCrewMembers().getValues().stream()
.filter(member -> member.getMemberId().equals(홍석_ID))
.findFirst()
.get();
assertThat(expelledMember.isExpelled()).isTrue();
}

@Test
void 리더가_아니면_크루원_추방에_실패한다() {
// given
Crew crew = createCrewWithMutableMembers(LEADER_ID);

// when, then
assertThatThrownBy(() -> crew.getCrewMembers().expel(홍석_ID, 정수_ID))
.isExactlyInstanceOf(NotCrewLeaderException.class);
}

@Test
void 리더가_자기_자신을_추방하면_실패한다() {
// given
Crew crew = createCrewWithMutableMembers(LEADER_ID);

// when, then
assertThatThrownBy(() -> crew.getCrewMembers().expel(LEADER_ID, LEADER_ID))
.isExactlyInstanceOf(CrewMemberExpelFailedException.class);
}
}
2 changes: 1 addition & 1 deletion src/test/java/com/retrip/crew/domain/entity/CrewTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class CrewTest {
"속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.",
100,
LEADER_ID,
List.of("질문1"))
List.of("속초 맛집을 아시나요?"))
).doesNotThrowAnyException();
}

Expand Down
Loading