diff --git a/src/main/java/com/retrip/crew/application/in/CrewService.java b/src/main/java/com/retrip/crew/application/in/CrewService.java index ed978f0..33bae4b 100644 --- a/src/main/java/com/retrip/crew/application/in/CrewService.java +++ b/src/main/java/com/retrip/crew/application/in/CrewService.java @@ -1,14 +1,20 @@ package com.retrip.crew.application.in; -import com.retrip.crew.application.in.request.CreateDemandRequest; -import com.retrip.crew.application.in.request.CrewCreateRequest; -import com.retrip.crew.application.in.request.CrewOrder; -import com.retrip.crew.application.in.request.CrewUpdateRequest; -import com.retrip.crew.application.in.response.*; +import com.retrip.crew.application.in.request.crew.CrewCreateRequest; +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; +import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.crew.CrewCreateResponse; +import com.retrip.crew.application.in.response.crew.CrewDetailResponse; +import com.retrip.crew.application.in.response.crew.CrewListResponse; +import com.retrip.crew.application.in.response.crew.CrewUpdateResponse; +import com.retrip.crew.application.in.response.demand.*; import com.retrip.crew.application.in.usecase.GetCrewUseCase; import com.retrip.crew.application.in.usecase.ManageCrewUseCase; import com.retrip.crew.application.in.usecase.ManageDemandUseCase; import com.retrip.crew.application.in.usecase.UpdateRecruitmentUseCase; +import com.retrip.crew.application.out.repository.CrewDemandRepository; import com.retrip.crew.application.out.repository.CrewMemberRepository; import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; @@ -16,11 +22,16 @@ import com.retrip.crew.domain.entity.Demand; import com.retrip.crew.domain.entity.Recruitment; import com.retrip.crew.domain.exception.CrewNotFoundException; +import com.retrip.crew.domain.exception.NotCrewLeaderException; +import com.retrip.crew.domain.exception.common.BusinessException; +import com.retrip.crew.domain.exception.common.EntityNotFoundException; import com.retrip.crew.domain.vo.CrewDescription; import com.retrip.crew.domain.vo.CrewTitle; +import com.retrip.crew.domain.vo.DemandStatus; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; import com.retrip.crew.infra.util.PaginationUtils; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -35,6 +46,7 @@ public class CrewService implements ManageCrewUseCase, UpdateRecruitmentUseCase, private final CrewRepository crewRepository; private final CrewMemberRepository crewMemberRepository; private final CrewQueryRepository crewQueryRepository; + private final CrewDemandRepository demandRepository; @Override public CrewCreateResponse createCrew(CrewCreateRequest request) { @@ -75,6 +87,59 @@ public CreateDemandResponse createDemand(UUID crewId, CreateDemandRequest reques return CreateDemandResponse.of(crew.getId(), demand); } + @Override + public Page getDemands( + UUID crewId, UUID memberId, String status, Pageable pageable, DemandOrder order, String sort) { + Crew crew = findById(crewId); + throwIfNotLeader(crew, memberId, new NotCrewLeaderException()); + Page demands = demandRepository.findByCrewIdAndStatus( + crewId, DemandStatus.valueOf(status), PaginationUtils.createPageRequest(pageable, order.getField(), sort)); + return demands.map(d -> DemandsResponse.of(crewId, d)); + } + + @Override + public Page getCrewsOfDemand( + UUID crewId, UUID demandId, UUID memberId, Pageable pageable, CrewOrder order, String sort) { + Crew crew = findById(crewId); + throwIfNotLeader(crew, memberId, new NotCrewLeaderException()); + Demand demand = findDemandByIdAndCrewId(demandId, crewId); + return crewQueryRepository.findAllContainsMember(pageable, demand.getMemberId()); + } + + private static void throwIfNotLeader(Crew crew, UUID memberId, BusinessException exception) { + if (!crew.getCrewMembers().isLeader(memberId)) { + throw exception; + } + } + + @Override + public void cancelDemand(UUID crewId, UUID demandId, UUID memberId) { + Demand demand = findDemandByIdAndCrewId(demandId, crewId); + Crew crew = demand.getCrew(); + crew.cancelDemand(demand); + } + + @Override + public RejectDemandResponse rejectDemand(UUID crewId, UUID demandId, UUID memberId) { + Demand demand = findDemandByIdAndCrewId(demandId, crewId); + Crew crew = demand.getCrew(); + crew.rejectDemand(demand); + return RejectDemandResponse.of(demand); + } + + @Override + public ApproveDemandResponse approveDemand(UUID crewId, UUID demandId, UUID memberId) { + Demand demand = findDemandByIdAndCrewId(demandId, crewId); + Crew crew = demand.getCrew(); + crew.approveDemand(demand); + return ApproveDemandResponse.of(demand); + } + + private Demand findDemandByIdAndCrewId(UUID demandId, UUID crewId) { + return demandRepository.findCrewByIdAndCrewId(demandId, crewId) + .orElseThrow(() -> new EntityNotFoundException("참여 요청 엔티티를 찾을 수 없습니다.")); + } + @Override @Transactional(readOnly = true) public ScrollPageResponse getCrews(Pageable pageable, String keyword, CrewOrder order, String sort) { diff --git a/src/main/java/com/retrip/crew/application/in/request/CreateDemandRequest.java b/src/main/java/com/retrip/crew/application/in/request/CreateDemandRequest.java deleted file mode 100644 index 537f1ea..0000000 --- a/src/main/java/com/retrip/crew/application/in/request/CreateDemandRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.retrip.crew.application.in.request; - -import java.util.UUID; - -public record CreateDemandRequest( - UUID memberId -) { -} diff --git a/src/main/java/com/retrip/crew/application/in/request/CrewCreateRequest.java b/src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java similarity index 93% rename from src/main/java/com/retrip/crew/application/in/request/CrewCreateRequest.java rename to src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java index 145bd3a..ce499e2 100644 --- a/src/main/java/com/retrip/crew/application/in/request/CrewCreateRequest.java +++ b/src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.request; +package com.retrip.crew.application.in.request.crew; import com.retrip.crew.domain.entity.Crew; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/retrip/crew/application/in/request/CrewOrder.java b/src/main/java/com/retrip/crew/application/in/request/crew/CrewOrder.java similarity index 77% rename from src/main/java/com/retrip/crew/application/in/request/CrewOrder.java rename to src/main/java/com/retrip/crew/application/in/request/crew/CrewOrder.java index 4baabff..6a8de25 100644 --- a/src/main/java/com/retrip/crew/application/in/request/CrewOrder.java +++ b/src/main/java/com/retrip/crew/application/in/request/crew/CrewOrder.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.request; +package com.retrip.crew.application.in.request.crew; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/retrip/crew/application/in/request/CrewUpdateRequest.java b/src/main/java/com/retrip/crew/application/in/request/crew/CrewUpdateRequest.java similarity index 90% rename from src/main/java/com/retrip/crew/application/in/request/CrewUpdateRequest.java rename to src/main/java/com/retrip/crew/application/in/request/crew/CrewUpdateRequest.java index 9d5a509..eb94d66 100644 --- a/src/main/java/com/retrip/crew/application/in/request/CrewUpdateRequest.java +++ b/src/main/java/com/retrip/crew/application/in/request/crew/CrewUpdateRequest.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.request; +package com.retrip.crew.application.in.request.crew; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Size; diff --git a/src/main/java/com/retrip/crew/application/in/request/demand/CreateDemandRequest.java b/src/main/java/com/retrip/crew/application/in/request/demand/CreateDemandRequest.java new file mode 100644 index 0000000..44f3ec6 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/demand/CreateDemandRequest.java @@ -0,0 +1,11 @@ +package com.retrip.crew.application.in.request.demand; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "크루 참여 요청 생성 Request") +public record CreateDemandRequest( + UUID memberId +) { +} diff --git a/src/main/java/com/retrip/crew/application/in/request/demand/DemandOrder.java b/src/main/java/com/retrip/crew/application/in/request/demand/DemandOrder.java new file mode 100644 index 0000000..a83e287 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/demand/DemandOrder.java @@ -0,0 +1,12 @@ +package com.retrip.crew.application.in.request.demand; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DemandOrder { + DATE("createdAt"); + + private final String field; +} diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java b/src/main/java/com/retrip/crew/application/in/response/crew/CrewCreateResponse.java similarity index 95% rename from src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java rename to src/main/java/com/retrip/crew/application/in/response/crew/CrewCreateResponse.java index ad231f9..d928c7c 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/crew/CrewCreateResponse.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.response; +package com.retrip.crew.application.in.response.crew; import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.vo.RecruitmentStatus; diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java b/src/main/java/com/retrip/crew/application/in/response/crew/CrewDetailResponse.java similarity index 97% rename from src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java rename to src/main/java/com/retrip/crew/application/in/response/crew/CrewDetailResponse.java index bc95740..9fca2a1 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/crew/CrewDetailResponse.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.response; +package com.retrip.crew.application.in.response.crew; import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.entity.CrewMember; diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java b/src/main/java/com/retrip/crew/application/in/response/crew/CrewListResponse.java similarity index 91% rename from src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java rename to src/main/java/com/retrip/crew/application/in/response/crew/CrewListResponse.java index 7376a82..bf4588e 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/crew/CrewListResponse.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.response; +package com.retrip.crew.application.in.response.crew; import io.swagger.v3.oas.annotations.media.Schema; import java.util.UUID; diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewUpdateResponse.java b/src/main/java/com/retrip/crew/application/in/response/crew/CrewUpdateResponse.java similarity index 90% rename from src/main/java/com/retrip/crew/application/in/response/CrewUpdateResponse.java rename to src/main/java/com/retrip/crew/application/in/response/crew/CrewUpdateResponse.java index 2e513c6..28f8e08 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewUpdateResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/crew/CrewUpdateResponse.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.response; +package com.retrip.crew.application.in.response.crew; import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.vo.RecruitmentStatus; @@ -6,6 +6,7 @@ import java.util.UUID; +@Schema(description = "크루 정보 변경 Response") public record CrewUpdateResponse( @Schema(description = "크루 ID") UUID id, diff --git a/src/main/java/com/retrip/crew/application/in/response/demand/ApproveDemandResponse.java b/src/main/java/com/retrip/crew/application/in/response/demand/ApproveDemandResponse.java new file mode 100644 index 0000000..d7c9696 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/demand/ApproveDemandResponse.java @@ -0,0 +1,26 @@ +package com.retrip.crew.application.in.response.demand; + +import com.retrip.crew.domain.entity.Demand; +import com.retrip.crew.domain.vo.DemandStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "크루 참여 요청 승인 Response") +public record ApproveDemandResponse( + @Schema(description = "크루 ID") + UUID crewId, + + @Schema(description = "참여 요청 ID") + UUID demandId, + + @Schema(description = "참여 요청자 ID") + UUID memberId, + + @Schema(description = "참여 요청 상태") + DemandStatus status +) { + public static ApproveDemandResponse of(Demand demand) { + return new ApproveDemandResponse(demand.getCrew().getId(), demand.getId(), demand.getMemberId(), demand.getStatus()); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/ChangeRecruitmentStatusResponse.java b/src/main/java/com/retrip/crew/application/in/response/demand/ChangeRecruitmentStatusResponse.java similarity index 82% rename from src/main/java/com/retrip/crew/application/in/response/ChangeRecruitmentStatusResponse.java rename to src/main/java/com/retrip/crew/application/in/response/demand/ChangeRecruitmentStatusResponse.java index 0a56b73..2da6f59 100644 --- a/src/main/java/com/retrip/crew/application/in/response/ChangeRecruitmentStatusResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/demand/ChangeRecruitmentStatusResponse.java @@ -1,4 +1,4 @@ -package com.retrip.crew.application.in.response; +package com.retrip.crew.application.in.response.demand; import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.vo.RecruitmentStatus; @@ -6,6 +6,7 @@ import java.util.UUID; +@Schema(description = "크루 모집 상태 변경 Response") public record ChangeRecruitmentStatusResponse( @Schema(description = "크루 ID") UUID id, diff --git a/src/main/java/com/retrip/crew/application/in/response/CreateDemandResponse.java b/src/main/java/com/retrip/crew/application/in/response/demand/CreateDemandResponse.java similarity index 53% rename from src/main/java/com/retrip/crew/application/in/response/CreateDemandResponse.java rename to src/main/java/com/retrip/crew/application/in/response/demand/CreateDemandResponse.java index 1c42d65..2945e4f 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CreateDemandResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/demand/CreateDemandResponse.java @@ -1,11 +1,16 @@ -package com.retrip.crew.application.in.response; +package com.retrip.crew.application.in.response.demand; import com.retrip.crew.domain.entity.Demand; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.UUID; +@Schema(description = "크루 참여 요청 생성 Response") public record CreateDemandResponse( + @Schema(description = "크루 ID") UUID crewId, + + @Schema(description = "참여 요청자 ID") UUID memberId ) { public static CreateDemandResponse of(UUID crewId, Demand demand) { diff --git a/src/main/java/com/retrip/crew/application/in/response/demand/CrewsOfDemandResponse.java b/src/main/java/com/retrip/crew/application/in/response/demand/CrewsOfDemandResponse.java new file mode 100644 index 0000000..142e2cf --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/demand/CrewsOfDemandResponse.java @@ -0,0 +1,27 @@ +package com.retrip.crew.application.in.response.demand; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "참여자가 속한 크루 목록 조회 Response") +public record CrewsOfDemandResponse( + @Schema(description = "참여 요청자 ID") + UUID memberId, + + @Schema(description = "크루 ID") + UUID id, + + @Schema(description = "크루 타이틀") + String title, + + @Schema(description = "크루 리더 ID") + UUID leaderId, + + @Schema(description = "크루원 현재 인원수") + long memberCount, + + @Schema(description = "크루원 최대 인원수") + int maxMemberCount +) { +} diff --git a/src/main/java/com/retrip/crew/application/in/response/demand/DemandsResponse.java b/src/main/java/com/retrip/crew/application/in/response/demand/DemandsResponse.java new file mode 100644 index 0000000..651289f --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/demand/DemandsResponse.java @@ -0,0 +1,26 @@ +package com.retrip.crew.application.in.response.demand; + +import com.retrip.crew.domain.entity.Demand; +import com.retrip.crew.domain.vo.DemandStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "참여 요청 목록 조회 Response") +public record DemandsResponse( + @Schema(description = "크루 ID") + UUID crewId, + + @Schema(description = "참여 요청 ID") + UUID demandId, + + @Schema(description = "참여 요청자 ID") + UUID memberId, + + @Schema(description = "참여 요청 상태") + DemandStatus status +) { + public static DemandsResponse of(UUID crewId, Demand demand) { + return new DemandsResponse(crewId, demand.getId(), demand.getMemberId(), demand.getStatus()); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/demand/RejectDemandResponse.java b/src/main/java/com/retrip/crew/application/in/response/demand/RejectDemandResponse.java new file mode 100644 index 0000000..3e9f00a --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/demand/RejectDemandResponse.java @@ -0,0 +1,26 @@ +package com.retrip.crew.application.in.response.demand; + +import com.retrip.crew.domain.entity.Demand; +import com.retrip.crew.domain.vo.DemandStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "크루 참여 요청 거절 Response") +public record RejectDemandResponse( + @Schema(description = "크루 ID") + UUID crewId, + + @Schema(description = "참여 요청 ID") + UUID demandId, + + @Schema(description = "참여 요청자 ID") + UUID memberId, + + @Schema(description = "참여 요청 상태") + DemandStatus status +) { + public static RejectDemandResponse of(Demand demand) { + return new RejectDemandResponse(demand.getCrew().getId(), demand.getId(), demand.getMemberId(), demand.getStatus()); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java index 34b9bc4..c158c73 100644 --- a/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java +++ b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java @@ -1,8 +1,8 @@ package com.retrip.crew.application.in.usecase; -import com.retrip.crew.application.in.request.CrewOrder; -import com.retrip.crew.application.in.response.CrewDetailResponse; -import com.retrip.crew.application.in.response.CrewListResponse; +import com.retrip.crew.application.in.request.crew.CrewOrder; +import com.retrip.crew.application.in.response.crew.CrewDetailResponse; +import com.retrip.crew.application.in.response.crew.CrewListResponse; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; import java.util.UUID; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java index b8fb910..61df32d 100644 --- a/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java @@ -1,9 +1,9 @@ package com.retrip.crew.application.in.usecase; -import com.retrip.crew.application.in.request.CrewCreateRequest; -import com.retrip.crew.application.in.request.CrewUpdateRequest; -import com.retrip.crew.application.in.response.CrewCreateResponse; -import com.retrip.crew.application.in.response.CrewUpdateResponse; +import com.retrip.crew.application.in.request.crew.CrewCreateRequest; +import com.retrip.crew.application.in.request.crew.CrewUpdateRequest; +import com.retrip.crew.application.in.response.crew.CrewCreateResponse; +import com.retrip.crew.application.in.response.crew.CrewUpdateResponse; import java.util.UUID; diff --git a/src/main/java/com/retrip/crew/application/in/usecase/ManageDemandUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/ManageDemandUseCase.java index ae7008f..0f0c976 100644 --- a/src/main/java/com/retrip/crew/application/in/usecase/ManageDemandUseCase.java +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageDemandUseCase.java @@ -1,10 +1,25 @@ package com.retrip.crew.application.in.usecase; -import com.retrip.crew.application.in.request.CreateDemandRequest; -import com.retrip.crew.application.in.response.CreateDemandResponse; +import com.retrip.crew.application.in.request.demand.CreateDemandRequest; +import com.retrip.crew.application.in.request.crew.CrewOrder; +import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.demand.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import java.util.UUID; public interface ManageDemandUseCase { CreateDemandResponse createDemand(UUID crewId, CreateDemandRequest request); + + Page getDemands( + UUID crewId, UUID memberId, String status, Pageable pageable, DemandOrder order, String sort); + + Page getCrewsOfDemand(UUID crewId, UUID demandId, UUID memberId, Pageable pageable, CrewOrder order, String sort); + + void cancelDemand(UUID crewId, UUID demandId, UUID memberId); + + RejectDemandResponse rejectDemand(UUID crewId, UUID demandId, UUID memberId); + + ApproveDemandResponse approveDemand(UUID crewId, UUID demandId, UUID memberId); } diff --git a/src/main/java/com/retrip/crew/application/in/usecase/UpdateRecruitmentUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/UpdateRecruitmentUseCase.java index f96e831..4f03516 100644 --- a/src/main/java/com/retrip/crew/application/in/usecase/UpdateRecruitmentUseCase.java +++ b/src/main/java/com/retrip/crew/application/in/usecase/UpdateRecruitmentUseCase.java @@ -1,6 +1,6 @@ package com.retrip.crew.application.in.usecase; -import com.retrip.crew.application.in.response.ChangeRecruitmentStatusResponse; +import com.retrip.crew.application.in.response.demand.ChangeRecruitmentStatusResponse; import java.util.UUID; diff --git a/src/main/java/com/retrip/crew/application/out/repository/CrewDemandRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewDemandRepository.java new file mode 100644 index 0000000..9d9f8d9 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/out/repository/CrewDemandRepository.java @@ -0,0 +1,18 @@ +package com.retrip.crew.application.out.repository; + +import com.retrip.crew.domain.entity.Demand; +import com.retrip.crew.domain.vo.DemandStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; +import java.util.UUID; + +public interface CrewDemandRepository extends JpaRepository { + Page findByCrewIdAndStatus(UUID crewId, DemandStatus pending, Pageable pageRequest); + + @Query("select d, c from Demand d join fetch d.crew c where d.id = :demandId and c.id = :crewId") + Optional findCrewByIdAndCrewId(UUID demandId, UUID crewId); +} diff --git a/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java index 6a1c089..836dcec 100644 --- a/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java +++ b/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java @@ -1,10 +1,16 @@ package com.retrip.crew.application.out.repository; -import com.retrip.crew.application.in.response.CrewListResponse; +import com.retrip.crew.application.in.response.crew.CrewListResponse; +import com.retrip.crew.application.in.response.demand.CrewsOfDemandResponse; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import java.util.UUID; + public interface CrewQueryRepository { Slice getCrews(Pageable pageable, String keyword); Long getCrewCount(String keyword); + + Page findAllContainsMember(Pageable pageable, UUID memberId); } diff --git a/src/main/java/com/retrip/crew/domain/converter/DemandStatusConverter.java b/src/main/java/com/retrip/crew/domain/converter/DemandStatusConverter.java new file mode 100644 index 0000000..13d351b --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/converter/DemandStatusConverter.java @@ -0,0 +1,24 @@ +package com.retrip.crew.domain.converter; + +import com.retrip.crew.domain.vo.DemandStatus; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class DemandStatusConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(DemandStatus status) { + if(status == null){ + throw new NullPointerException("DemandStatus를 DB 칼럼으로 변경하는 과정에서 null이 포함되었습니다."); + } + return status.getCode(); + } + + @Override + public DemandStatus convertToEntityAttribute(String dbData) { + if(dbData == null){ + throw new NullPointerException("DemandStatus 테이블의 role 값이 null입니다."); + } + return DemandStatus.codeOf(dbData); + } +} diff --git a/src/main/java/com/retrip/crew/domain/converter/RecruitmentStatusConverter.java b/src/main/java/com/retrip/crew/domain/converter/RecruitmentStatusConverter.java new file mode 100644 index 0000000..8c36ea7 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/converter/RecruitmentStatusConverter.java @@ -0,0 +1,24 @@ +package com.retrip.crew.domain.converter; + +import com.retrip.crew.domain.vo.RecruitmentStatus; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class RecruitmentStatusConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(RecruitmentStatus status) { + if(status == null){ + throw new NullPointerException("RecruitmentStatus를 DB 칼럼으로 변경하는 과정에서 null이 포함되었습니다."); + } + return status.getCode(); + } + + @Override + public RecruitmentStatus convertToEntityAttribute(String dbData) { + if(dbData == null){ + throw new NullPointerException("RecruitmentStatus 테이블의 role 값이 null입니다."); + } + return RecruitmentStatus.codeOf(dbData); + } +} diff --git a/src/main/java/com/retrip/crew/domain/entity/Announcements.java b/src/main/java/com/retrip/crew/domain/entity/Announcements.java index 9a85944..e354fd6 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Announcements.java +++ b/src/main/java/com/retrip/crew/domain/entity/Announcements.java @@ -3,9 +3,7 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Embeddable; import jakarta.persistence.OneToMany; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/retrip/crew/domain/entity/Crew.java b/src/main/java/com/retrip/crew/domain/entity/Crew.java index fd2180e..b3898d0 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -82,4 +82,16 @@ public String getDescription(){ return description.getValue(); } + public void cancelDemand(Demand demand) { + recruitment.cancelDemand(demand); + } + + public void rejectDemand(Demand demand) { + recruitment.rejectDemand(demand); + } + + public void approveDemand(Demand demand) { + recruitment.approveDemand(demand); + crewMembers.addMember(demand, this); + } } diff --git a/src/main/java/com/retrip/crew/domain/entity/CrewMember.java b/src/main/java/com/retrip/crew/domain/entity/CrewMember.java index 7a2c6f1..fd290b2 100644 --- a/src/main/java/com/retrip/crew/domain/entity/CrewMember.java +++ b/src/main/java/com/retrip/crew/domain/entity/CrewMember.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; import java.util.UUID; @Entity @@ -37,4 +36,8 @@ public CrewMember(Crew crew, UUID memberId, CrewMemberRole crewMemberRole) { this.memberId = memberId; this.crewMemberRole = CrewMemberRole.valueOf(crewMemberRole.name()); } + + public boolean isLeader() { + return CrewMemberRole.isLeaderRole(this.crewMemberRole); + } } diff --git a/src/main/java/com/retrip/crew/domain/entity/CrewMemberRole.java b/src/main/java/com/retrip/crew/domain/entity/CrewMemberRole.java index 86072f2..bd39ab5 100644 --- a/src/main/java/com/retrip/crew/domain/entity/CrewMemberRole.java +++ b/src/main/java/com/retrip/crew/domain/entity/CrewMemberRole.java @@ -21,5 +21,9 @@ public static CrewMemberRole codeOf(String code) { .findAny() .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 코드입니다.")); } + + public static boolean isLeaderRole(CrewMemberRole role) { + return LEADER == role; + } } diff --git a/src/main/java/com/retrip/crew/domain/entity/CrewMembers.java b/src/main/java/com/retrip/crew/domain/entity/CrewMembers.java index 4a358b1..eab4d38 100644 --- a/src/main/java/com/retrip/crew/domain/entity/CrewMembers.java +++ b/src/main/java/com/retrip/crew/domain/entity/CrewMembers.java @@ -1,5 +1,7 @@ package com.retrip.crew.domain.entity; +import com.retrip.crew.domain.exception.common.IllegalStateException; +import com.retrip.crew.domain.exception.common.InvalidValueException; import jakarta.persistence.CascadeType; import jakarta.persistence.Embeddable; import jakarta.persistence.OneToMany; @@ -18,12 +20,12 @@ public class CrewMembers { @OneToMany(mappedBy = "crew", cascade = CascadeType.ALL, orphanRemoval = true) private List values = new ArrayList<>(); - public CrewMembers(Crew crew, UUID member) { - this.values = createLeader(crew, member); + public CrewMembers(Crew crew, UUID leaderId) { + this.values.add(createLeader(crew, leaderId)); } - private List createLeader(Crew crew, UUID memberId) { - return List.of(new CrewMember(crew, memberId, CrewMemberRole.LEADER)); + private CrewMember createLeader(Crew crew, UUID memberId) { + return new CrewMember(crew, memberId, CrewMemberRole.LEADER); } public CrewMember getLeader() { @@ -36,4 +38,25 @@ public CrewMember getLeader() { public int getSize() { return this.values.size(); } + + public boolean isLeader(UUID memberId) { + return this.values.stream() + .filter(m -> memberId.equals(m.getMemberId())) + .findFirst() + .orElseThrow(() -> new InvalidValueException("크루 멤버가 아닙니다.")) + .isLeader(); + } + + public void addMember(Demand demand, Crew crew) { + if (isDuplicate(demand)) { + 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())); + } } diff --git a/src/main/java/com/retrip/crew/domain/entity/Demand.java b/src/main/java/com/retrip/crew/domain/entity/Demand.java index 8fae633..59a2257 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Demand.java +++ b/src/main/java/com/retrip/crew/domain/entity/Demand.java @@ -1,5 +1,6 @@ package com.retrip.crew.domain.entity; +import com.retrip.crew.domain.vo.DemandStatus; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -7,14 +8,17 @@ import java.util.UUID; +import static com.retrip.crew.domain.vo.DemandStatus.*; + @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED, force = true) @Getter -public class Demand { +public class Demand extends BaseEntity { @Id @Column(columnDefinition = "varbinary(16)") private UUID id; private UUID memberId; + private DemandStatus status; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn( @@ -28,6 +32,35 @@ public class Demand { public Demand(UUID memberId, Crew crew) { this.id = UUID.randomUUID(); this.memberId = memberId; + this.status = PENDING; this.crew = crew; } + + public boolean isNotPending() { + return this.status != PENDING; + } + + public void cancel() { + this.status = CANCELED; + } + + public void approve() { + this.status = APPROVED; + } + + public void reject() { + this.status = REJECTED; + } + + public boolean isCanceled() { + return this.status == CANCELED; + } + + public boolean isEqualTo(UUID memberId) { + return this.memberId.equals(memberId); + } + + public void restore() { + this.status = PENDING; + } } diff --git a/src/main/java/com/retrip/crew/domain/entity/Recruitment.java b/src/main/java/com/retrip/crew/domain/entity/Recruitment.java index e445277..50d14db 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Recruitment.java +++ b/src/main/java/com/retrip/crew/domain/entity/Recruitment.java @@ -1,8 +1,12 @@ package com.retrip.crew.domain.entity; -import com.retrip.crew.domain.exception.common.IllegalStateException; +import com.retrip.crew.domain.exception.DuplicateDemandException; +import com.retrip.crew.domain.exception.IllegalDemandStateException; +import com.retrip.crew.domain.exception.UnableToStartRecruitmentException; +import com.retrip.crew.domain.exception.common.InvalidValueException; import com.retrip.crew.domain.vo.RecruitmentStatus; import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import jakarta.persistence.OneToMany; import lombok.AccessLevel; @@ -11,6 +15,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import static com.retrip.crew.domain.vo.RecruitmentStatus.RECRUITING; @@ -21,6 +26,8 @@ @Embeddable public class Recruitment { private int maxMembers; + + @Column(name = "recruitment_status") private RecruitmentStatus status; @OneToMany(mappedBy = "crew", cascade = CascadeType.ALL, orphanRemoval = true) @@ -34,7 +41,7 @@ public Recruitment(int maxMembers) { public void start(int membersSize) { if (isRecruitmentComplete(membersSize)) { stop(); - throw new IllegalStateException("최대 인원을 모두 모집 완료하여 더 이상 멤버를 모집할 수 없습니다."); + throw new UnableToStartRecruitmentException("최대 인원을 모두 모집 완료하여 더 이상 멤버를 모집할 수 없습니다."); } this.status = RECRUITING; } @@ -44,7 +51,7 @@ public void stop() { } private boolean isRecruitmentComplete(int membersSize) { - return this.maxMembers == membersSize; + return this.maxMembers <= membersSize; } public void updateMaxMembers(int maxMembers) { @@ -53,7 +60,12 @@ public void updateMaxMembers(int maxMembers) { public Demand addDemand(UUID memberId, Crew crew) { if (isDuplicate(memberId)) { - throw new IllegalStateException("이미 요청한 사용자는 다시 요청할 수 없습니다."); + throw new DuplicateDemandException(); + } + Optional reDemand = findReDemand(memberId); + if (reDemand.isPresent()) { + reDemand.get().restore(); + return reDemand.get(); } Demand demand = new Demand(memberId, crew); demands.add(demand); @@ -62,7 +74,48 @@ public Demand addDemand(UUID memberId, Crew crew) { private boolean isDuplicate(UUID memberId) { return demands.stream() - .map(Demand::getMemberId) - .anyMatch(id -> id.equals(memberId)); + .anyMatch(d -> d.isEqualTo(memberId) && !d.isCanceled()); + } + + private Optional findReDemand(UUID memberId) { + return demands.stream() + .filter(d -> isReDemand(memberId, d)) + .findFirst(); + } + + private static boolean isReDemand(UUID memberId, Demand demand) { + return demand.isEqualTo(memberId) + && demand.isCanceled(); + } + + public void cancelDemand(Demand demand) { + Demand find = findDemand(demand); + throwIfNotPending(find); + find.cancel(); + } + + public void approveDemand(Demand demand) { + Demand find = findDemand(demand); + throwIfNotPending(find); + find.approve(); + } + + public void rejectDemand(Demand demand) { + Demand find = findDemand(demand); + throwIfNotPending(find); + find.reject(); + } + + private static void throwIfNotPending(Demand find) { + if (find.isNotPending()) { + throw new IllegalDemandStateException("참여 요청의 상태가 대기중이 아닙니다."); + } + } + + private Demand findDemand(Demand demand) { + return demands.stream() + .filter(d -> d.equals(demand)) + .findFirst() + .orElseThrow(() -> new InvalidValueException("참여 요청을 찾을 수 없습니다.")); } } diff --git a/src/main/java/com/retrip/crew/domain/exception/DuplicateDemandException.java b/src/main/java/com/retrip/crew/domain/exception/DuplicateDemandException.java new file mode 100644 index 0000000..bb7a544 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/DuplicateDemandException.java @@ -0,0 +1,11 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.ErrorCode; +import com.retrip.crew.domain.exception.common.IllegalStateException; + +public class DuplicateDemandException extends IllegalStateException { + private static final ErrorCode errorCode = ErrorCode.DUPLICATE_DEMAND; + public DuplicateDemandException() { + super(errorCode); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/IllegalDemandStateException.java b/src/main/java/com/retrip/crew/domain/exception/IllegalDemandStateException.java new file mode 100644 index 0000000..2307fa7 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/IllegalDemandStateException.java @@ -0,0 +1,11 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.ErrorCode; +import com.retrip.crew.domain.exception.common.IllegalStateException; + +public class IllegalDemandStateException extends IllegalStateException { + private static final ErrorCode errorCode = ErrorCode.ILLEGAL_DEMAND_STATE; + public IllegalDemandStateException(String message) { + super(errorCode, message); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/NotCrewLeaderException.java b/src/main/java/com/retrip/crew/domain/exception/NotCrewLeaderException.java new file mode 100644 index 0000000..f035c07 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/NotCrewLeaderException.java @@ -0,0 +1,12 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.ErrorCode; +import com.retrip.crew.domain.exception.common.InvalidValueException; + +public class NotCrewLeaderException extends InvalidValueException { + private static final ErrorCode errorCode = ErrorCode.NOT_CREW_LEADER; + + public NotCrewLeaderException() { + super(errorCode); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/UnableToStartRecruitmentException.java b/src/main/java/com/retrip/crew/domain/exception/UnableToStartRecruitmentException.java new file mode 100644 index 0000000..160410d --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/UnableToStartRecruitmentException.java @@ -0,0 +1,11 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.ErrorCode; +import com.retrip.crew.domain.exception.common.IllegalStateException; + +public class UnableToStartRecruitmentException extends IllegalStateException { + private static final ErrorCode errorCode = ErrorCode.UNABLE_TO_START_RECRUITMENT; + public UnableToStartRecruitmentException(String message) { + super(errorCode, message); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java b/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java index 4d05d8e..4f2ae0b 100644 --- a/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java +++ b/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java @@ -13,7 +13,11 @@ public enum ErrorCode { ENTITY_NOT_FOUND(BAD_REQUEST, "Common-004", "Entity not found"), ILLEGAL_STATE(BAD_REQUEST, "Common-005", "Illegal state"), - CREW_NOT_FOUND(BAD_REQUEST, "Crew-001", "크루 엔티티를 찾을 수 없습니다.") + CREW_NOT_FOUND(BAD_REQUEST, "Crew-001", "크루 엔티티를 찾을 수 없습니다."), + NOT_CREW_LEADER(BAD_REQUEST, "Crew-002", "크루 리더가 아니면 접근할 수 없습니다."), + UNABLE_TO_START_RECRUITMENT(BAD_REQUEST, "Crew-003", "크루 멤버를 모집할 수 없습니다."), + DUPLICATE_DEMAND(BAD_REQUEST, "Crew-004", "이미 크루 참여를 요청한 사용자는 다시 요청할 수 없습니다."), + ILLEGAL_DEMAND_STATE(BAD_REQUEST, "Crew-004", "이미 크루 참여를 요청한 사용자는 다시 요청할 수 없습니다.") ; private final HttpStatus status; diff --git a/src/main/java/com/retrip/crew/domain/vo/DemandStatus.java b/src/main/java/com/retrip/crew/domain/vo/DemandStatus.java new file mode 100644 index 0000000..2775da1 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/vo/DemandStatus.java @@ -0,0 +1,25 @@ +package com.retrip.crew.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum DemandStatus { + PENDING("PENDING", "대기"), + APPROVED("APPROVED", "승인"), + REJECTED("REJECTED", "거절"), + CANCELED("CANCELED", "취소"); + + private final String code; + private final String viewName; + + public static DemandStatus codeOf(String code) { + return Arrays.stream(DemandStatus.values()) + .filter(participantStatus -> participantStatus.getCode().equals(code)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 코드입니다.")); + } +} diff --git a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java index 573ff92..5dcfa2d 100644 --- a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java +++ b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java @@ -1,10 +1,15 @@ package com.retrip.crew.infra.adapter.in.presentation.rest; -import com.retrip.crew.application.in.request.CreateDemandRequest; -import com.retrip.crew.application.in.request.CrewCreateRequest; -import com.retrip.crew.application.in.request.CrewOrder; -import com.retrip.crew.application.in.request.CrewUpdateRequest; -import com.retrip.crew.application.in.response.*; +import com.retrip.crew.application.in.request.crew.CrewCreateRequest; +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; +import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.crew.CrewCreateResponse; +import com.retrip.crew.application.in.response.crew.CrewDetailResponse; +import com.retrip.crew.application.in.response.crew.CrewListResponse; +import com.retrip.crew.application.in.response.crew.CrewUpdateResponse; +import com.retrip.crew.application.in.response.demand.*; import com.retrip.crew.application.in.usecase.GetCrewUseCase; import com.retrip.crew.application.in.usecase.ManageCrewUseCase; import com.retrip.crew.application.in.usecase.ManageDemandUseCase; @@ -14,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @@ -27,8 +33,6 @@ @Tag(name = "Crew", description = "크루 서비스") public class CrewController { private final ManageCrewUseCase manageCrewUseCase; - private final UpdateRecruitmentUseCase updateRecruitmentUseCase; - private final ManageDemandUseCase manageDemandUseCase; private final GetCrewUseCase getCrewUseCase; @PostMapping @@ -46,29 +50,6 @@ public ApiResponse updateCrew( return ApiResponse.ok(crew); } - @PutMapping("/{crewId}/recruitments/start") - @Schema(description = "크루 모집 시작") - public ApiResponse startRecruitment(@PathVariable final UUID crewId) { - ChangeRecruitmentStatusResponse recruitment = updateRecruitmentUseCase.startRecruitment(crewId); - return ApiResponse.ok(recruitment); - } - - @PutMapping("/{crewId}/recruitments/stop") - @Schema(description = "크루 모집 중지") - public ApiResponse stopRecruitment(@PathVariable final UUID crewId) { - ChangeRecruitmentStatusResponse recruitment = updateRecruitmentUseCase.stopRecruitment(crewId); - return ApiResponse.ok(recruitment); - } - - @PostMapping("/{crewId}/demands") - @Schema(description = "크루 참여 요청") - public ApiResponse createDemand( - @PathVariable final UUID crewId, - @RequestBody CreateDemandRequest request) { - CreateDemandResponse demand = manageDemandUseCase.createDemand(crewId, request); - return ApiResponse.created(demand); - } - @GetMapping @Schema(description = "크루 리스트 조회") public ResponseEntity>> getCrews( diff --git a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/DemandController.java b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/DemandController.java new file mode 100644 index 0000000..6337344 --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/DemandController.java @@ -0,0 +1,108 @@ +package com.retrip.crew.infra.adapter.in.presentation.rest; + +import com.retrip.crew.application.in.request.crew.CrewOrder; +import com.retrip.crew.application.in.request.demand.CreateDemandRequest; +import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.demand.*; +import com.retrip.crew.application.in.usecase.ManageDemandUseCase; +import com.retrip.crew.application.in.usecase.UpdateRecruitmentUseCase; +import com.retrip.crew.infra.adapter.in.presentation.rest.common.ApiResponse; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RequiredArgsConstructor +@RequestMapping("/crews") +@RestController +@Tag(name = "Crew", description = "크루 참여 모집 서비스") +public class DemandController { + private final UpdateRecruitmentUseCase updateRecruitmentUseCase; + private final ManageDemandUseCase manageDemandUseCase; + + @PutMapping("/{crewId}/recruitments/start") + @Schema(description = "크루 모집 시작") + public ApiResponse startRecruitment(@PathVariable final UUID crewId) { + ChangeRecruitmentStatusResponse recruitment = updateRecruitmentUseCase.startRecruitment(crewId); + return ApiResponse.ok(recruitment); + } + + @PutMapping("/{crewId}/recruitments/stop") + @Schema(description = "크루 모집 중지") + public ApiResponse stopRecruitment(@PathVariable final UUID crewId) { + ChangeRecruitmentStatusResponse recruitment = updateRecruitmentUseCase.stopRecruitment(crewId); + return ApiResponse.ok(recruitment); + } + + @PostMapping("/{crewId}/demands") + @Schema(description = "크루 참여 요청") + public ApiResponse createDemand( + @PathVariable final UUID crewId, + @RequestBody CreateDemandRequest request) { + CreateDemandResponse demand = manageDemandUseCase.createDemand(crewId, request); + return ApiResponse.created(demand); + } + + @PutMapping("/{crewId}/demands/{demandId}/cancel") + @Schema(description = "크루 참여 요청 취소") + public ApiResponse cancelDemand( + @PathVariable final UUID crewId, + @PathVariable final UUID demandId, + @RequestParam final UUID memberId) { + manageDemandUseCase.cancelDemand(crewId, demandId, memberId); + return ApiResponse.noContent(); + } + + @GetMapping("/{crewId}/demands") + @Schema(description = "크루 참여 요청 목록 조회") + public ApiResponse> getDemands( + @PathVariable final UUID crewId, + @RequestParam final UUID memberId, + @RequestParam final String status, + @PageableDefault(size = 10) Pageable pageable, + @RequestParam(name = "order", defaultValue = "DATE") DemandOrder order, + @RequestParam(name = "sort", defaultValue = "asc") String sort) { + Page demands = + manageDemandUseCase.getDemands(crewId, memberId, status, pageable, order, sort); + return ApiResponse.ok(demands); + } + + @GetMapping("/{crewId}/demands/{demandId}/crews") + @Schema(description = "크루 참여 요청자가 속한 크루 목록 조회") + public ApiResponse> getCrewsOfDemand( + @PathVariable final UUID crewId, + @PathVariable final UUID demandId, + @RequestParam final UUID memberId, + @PageableDefault(size = 10) Pageable pageable, + @RequestParam(name = "order", defaultValue = "DATE") CrewOrder order, + @RequestParam(name = "sort", defaultValue = "asc") String sort) { + Page demands = + manageDemandUseCase.getCrewsOfDemand(crewId, demandId, memberId, pageable, order, sort); + return ApiResponse.ok(demands); + } + + @PutMapping("/{crewId}/demands/{demandId}/approve") + @Schema(description = "크루 참여 요청 승인") + public ApiResponse approveDemand( + @PathVariable final UUID crewId, + @PathVariable final UUID demandId, + @RequestParam final UUID memberId) { + ApproveDemandResponse demand = manageDemandUseCase.approveDemand(crewId, demandId, memberId); + return ApiResponse.ok(demand); + } + + @PutMapping("/{crewId}/demands/{demandId}/reject") + @Schema(description = "크루 참여 요청 거절") + public ApiResponse rejectDemand( + @PathVariable final UUID crewId, + @PathVariable final UUID demandId, + @RequestParam final UUID memberId) { + RejectDemandResponse demand = manageDemandUseCase.rejectDemand(crewId, demandId, memberId); + return ApiResponse.ok(demand); + } +} diff --git a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ApiResponse.java b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ApiResponse.java index b35985d..8749da5 100644 --- a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ApiResponse.java +++ b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ApiResponse.java @@ -23,6 +23,10 @@ public static ApiResponse ok(T data) { return success(data, OK); } + public static ApiResponse noContent() { + return success(null, NO_CONTENT); + } + public static ApiResponse of(T data, HttpStatus status) { return success(data, status); } diff --git a/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java index 13e7a09..1f8f53c 100644 --- a/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java +++ b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java @@ -7,18 +7,25 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; -import com.retrip.crew.application.in.request.CrewOrder; -import com.retrip.crew.application.in.response.CrewListResponse; +import com.retrip.crew.application.in.request.crew.CrewOrder; +import com.retrip.crew.application.in.response.crew.CrewListResponse; +import com.retrip.crew.application.in.response.demand.CrewsOfDemandResponse; import com.retrip.crew.application.out.repository.CrewQueryRepository; +import com.retrip.crew.domain.entity.CrewMemberRole; import com.retrip.crew.domain.entity.QCrewMember; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.UUID; import static com.querydsl.jpa.JPAExpressions.select; import static com.retrip.crew.domain.entity.QCrew.crew; @@ -39,11 +46,16 @@ public Slice getCrews(Pageable pageable, String keyword) { List fetch = query.select(Projections.constructor(CrewListResponse.class, crew.id.as("id"), crew.title.value.as("title"), - crewMember.id.as("leaderId"), + JPAExpressions.select(subCrewMember.memberId.as("leaderId")) + .from(subCrewMember) + .where( + subCrewMember.crewMemberRole.eq(CrewMemberRole.LEADER), + subCrewMember.crew.id.eq(crewMember.crew.id) + ), ExpressionUtils.as( select(subCrewMember.count()) - .from(subCrewMember) - .where(subCrewMember.crew.id.eq(crew.id)) ,MemberCountAlias), + .from(subCrewMember) + .where(subCrewMember.crew.id.eq(crew.id)), MemberCountAlias), crew.recruitment.maxMembers.as("maxMemberCount") ) ) @@ -73,6 +85,53 @@ private BooleanExpression crewTitleContains(String title) { return title == null ? null : crew.title.value.contains(title); } + @Override + public Page findAllContainsMember(Pageable pageable, UUID memberId) { + OrderSpecifier orderSpecifier = createCrewOrderSpecifier(pageable); + Expressions.numberPath(Long.class, "memberCount"); + QCrewMember subCrewMember = new QCrewMember("subCrewMember"); + List result = + query.select( + Projections.constructor(CrewsOfDemandResponse.class, + crewMember.memberId.as("memberId"), + crew.id.as("id"), + crew.title.value.as("title"), + JPAExpressions.select(subCrewMember.memberId.as("leaderId")) + .from(subCrewMember) + .where( + subCrewMember.crewMemberRole.eq(CrewMemberRole.LEADER), + subCrewMember.crew.id.eq(crewMember.crew.id) + ), + JPAExpressions.select(subCrewMember.count().as("memberCount")) + .from(subCrewMember) + .where(subCrewMember.crew.id.eq(crew.id)), + crew.recruitment.maxMembers.as("maxMemberCount") + ) + ) + .from(crew) + .rightJoin(crewMember) + .on(crewMember.crew.id.eq(crew.id)) + .where( + crewMemberContains(memberId) + ) + .limit(pageable.getPageSize() + 1) + .offset(pageable.getOffset()) + .orderBy(orderSpecifier) + .fetch(); + + JPAQuery countQuery = query.select(crew.count()) + .from(crew) + .rightJoin(crewMember).on(crewMember.crew.id.eq(crew.id)) + .where( + crewMemberContains(memberId) + ); + return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); + } + + private BooleanExpression crewMemberContains(UUID memberId) { + return memberId == null ? null : crewMember.memberId.eq(memberId); + } + private OrderSpecifier createCrewOrderSpecifier(Pageable pageable) { if (!pageable.getSort().isEmpty()) { for (Sort.Order order : pageable.getSort()) { diff --git a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java index a58c76a..42c5e2a 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -1,25 +1,36 @@ package com.retrip.crew.application.in; -import com.retrip.crew.application.in.request.CreateDemandRequest; -import com.retrip.crew.application.in.request.CrewCreateRequest; -import com.retrip.crew.application.in.request.CrewOrder; -import com.retrip.crew.application.in.request.CrewUpdateRequest; -import com.retrip.crew.application.in.response.*; +import com.retrip.crew.application.in.request.crew.CrewCreateRequest; +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; +import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.crew.CrewCreateResponse; +import com.retrip.crew.application.in.response.crew.CrewDetailResponse; +import com.retrip.crew.application.in.response.crew.CrewListResponse; +import com.retrip.crew.application.in.response.crew.CrewUpdateResponse; +import com.retrip.crew.application.in.response.demand.CreateDemandResponse; +import com.retrip.crew.application.in.response.demand.CrewsOfDemandResponse; +import com.retrip.crew.application.in.response.demand.DemandsResponse; import com.retrip.crew.common.ServiceTest; import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.entity.CrewMemberRole; import com.retrip.crew.domain.entity.Demand; -import com.retrip.crew.domain.exception.common.IllegalStateException; +import com.retrip.crew.domain.exception.DuplicateDemandException; +import com.retrip.crew.domain.vo.DemandStatus; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; import java.util.List; import java.util.UUID; -import static com.retrip.crew.common.fixture.CrewFixture.createCrewRequest; -import static com.retrip.crew.common.fixture.CrewFixture.createMultipleCrews; +import static com.retrip.crew.common.fixture.CrewFixture.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -29,7 +40,7 @@ class CrewServiceTest extends ServiceTest { void 크루를_생성_한다() { //given CrewCreateRequest request = createCrewRequest( - MEMBER_ID, + LEADER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100 @@ -46,12 +57,7 @@ class CrewServiceTest extends ServiceTest { @Test void 크루의_제목_설명_최대_인원수를_수정한다() { // given - Crew crew = crewRepository.save(Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100, - MEMBER_ID - )); + Crew crew = crewRepository.save(createCrew(LEADER_ID)); CrewUpdateRequest request = new CrewUpdateRequest( "강릉 크루원 구함", "강릉 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", @@ -70,16 +76,14 @@ class CrewServiceTest extends ServiceTest { @Test void 크루_참여_요청을_생성한다() { - Crew crew = crewRepository.save(Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100, - MEMBER_ID - )); + // given + Crew crew = crewRepository.save(createCrew(LEADER_ID)); CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID); + // when CreateDemandResponse response = crewService.createDemand(crew.getId(), request); + // then List demands = crew.getRecruitment().getDemands(); assertAll( () -> assertThat(demands.size()).isEqualTo(1), @@ -89,24 +93,84 @@ class CrewServiceTest extends ServiceTest { @Test void 이미_요청한_사용자는_다시_크루에_요청할_수_없다() { - Crew crew = Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100, - MEMBER_ID - ); + // given + Crew crew = createCrew(LEADER_ID); crew.demand(MEMBER_ID); crew.demand(UUID.randomUUID()); crew.demand(UUID.randomUUID()); Crew save = crewRepository.save(crew); CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID); + // when, then assertThatThrownBy(() -> crewService.createDemand(save.getId(), request)) - .isExactlyInstanceOf(IllegalStateException.class); + .isExactlyInstanceOf(DuplicateDemandException.class); + } + + @Test + void 크루_참여_요청을_취소한다() { + // given + Crew crew = crewRepository.save(createCrew(LEADER_ID)); + Demand demand = crew.demand(MEMBER_ID); + + // when + crewService.cancelDemand(crew.getId(), demand.getId(), MEMBER_ID); + + // then + assertThat(demand.getStatus()).isEqualTo(DemandStatus.CANCELED); + } + + @ParameterizedTest + @CsvSource({"PENDING,2", "APPROVED,2", "REJECTED,1"}) + void 리더가_크루_요청_목록을_조회한다(String status, int expected) { + // given + Crew crew = crewRepository.save(createCrew(LEADER_ID)); + Demand 정수 = new Demand(정수_ID, crew); + Demand 홍석 = new Demand(홍석_ID, crew); + Demand 준호 = new Demand(준호_ID, crew); + Demand 지수 = new Demand(지수_ID, crew); + Demand 혁진 = new Demand(혁진_ID, crew); + List demands = List.of(정수, 홍석, 준호, 지수, 혁진); + + ReflectionTestUtils.setField(준호, "status", DemandStatus.APPROVED); + ReflectionTestUtils.setField(지수, "status", DemandStatus.APPROVED); + ReflectionTestUtils.setField(혁진, "status", DemandStatus.REJECTED); + ReflectionTestUtils.setField(crew.getRecruitment(), "demands", demands); + + Pageable pageable = PageRequest.of(0, 10); + + // when + Page response = + crewService.getDemands(crew.getId(), LEADER_ID, status, pageable, DemandOrder.DATE, "desc"); + + // then + assertThat(response.getTotalElements()).isEqualTo(expected); + } + + @Test + void 참여_요청자가_속한_크루_목록을_조회한다() { + // given + Crew crew = createCrew(LEADER_ID); + Demand demand = new Demand(홍석_ID, crew); + ReflectionTestUtils.setField(crew.getRecruitment(), "demands", List.of(demand)); + + crewRepository.save(crew); + crewRepository.save(createCrewWithMembers(MEMBER_ID)); + crewRepository.save(createCrewWithMembers(MEMBER_ID)); + crewRepository.save(createCrewWithMembers(MEMBER_ID)); + crewRepository.save(createCrewWithMembers(MEMBER_ID)); + crewRepository.save(createCrewWithMembers(MEMBER_ID)); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page response = + crewService.getCrewsOfDemand(crew.getId(), demand.getId(), LEADER_ID, pageable, CrewOrder.DATE, "desc"); + + // then + assertThat(response.getTotalElements()).isEqualTo(5); } @Test - void 크루를_검색_및_정렬_필터링하여_조회한다(){ + void 크루를_검색_및_정렬_필터링하여_조회한다() { //given List requests = createMultipleCrews( 10, @@ -136,7 +200,7 @@ class CrewServiceTest extends ServiceTest { } @Test - void 크루_상세를_조회한다(){ + void 크루_상세를_조회한다() { //given CrewCreateRequest request = createCrewRequest( MEMBER_ID, diff --git a/src/test/java/com/retrip/crew/common/ServiceTest.java b/src/test/java/com/retrip/crew/common/ServiceTest.java index a6452c3..cce29ed 100644 --- a/src/test/java/com/retrip/crew/common/ServiceTest.java +++ b/src/test/java/com/retrip/crew/common/ServiceTest.java @@ -1,6 +1,7 @@ package com.retrip.crew.common; import com.retrip.crew.application.in.CrewService; +import com.retrip.crew.application.out.repository.CrewDemandRepository; import com.retrip.crew.application.out.repository.CrewMemberRepository; import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; @@ -11,8 +12,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.util.UUID; - @DataJpaTest @Import(QuerydslConfig.class) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @@ -27,12 +26,13 @@ public class ServiceTest { @Autowired protected CrewQueryRepository crewQueryRepository; - protected CrewService crewService; + @Autowired + protected CrewDemandRepository demandRepository; - protected UUID MEMBER_ID = UUID.fromString("13c8ab91-76bc-4f70-93e9-89f1a65dc640"); + protected CrewService crewService; @BeforeEach void setUp() { - crewService = new CrewService(crewRepository, crewMemberRepository, crewQueryRepository); + crewService = new CrewService(crewRepository, crewMemberRepository, crewQueryRepository, demandRepository); } } diff --git a/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java index 34ee232..e634962 100644 --- a/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java +++ b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java @@ -1,12 +1,25 @@ package com.retrip.crew.common.fixture; -import com.retrip.crew.application.in.request.CrewCreateRequest; +import com.retrip.crew.application.in.request.crew.CrewCreateRequest; +import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.entity.CrewMember; +import com.retrip.crew.domain.entity.CrewMemberRole; +import com.retrip.crew.domain.entity.CrewMembers; +import org.springframework.test.util.ReflectionTestUtils; + import java.util.List; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; public abstract class CrewFixture { + public static final UUID LEADER_ID = UUID.fromString("caec62d1-f29d-477d-9743-292f48cc66bb"); + public static final UUID MEMBER_ID = UUID.fromString("85e05380-3693-4f3f-b74b-203715d15df8"); + public static final UUID 정수_ID = UUID.fromString("a7f7215b-081a-42f4-b3e2-f06393de2f8b"); + public static final UUID 홍석_ID = UUID.fromString("bf97d20b-d1f7-46a9-8362-11b9fa02d67d"); + public static final UUID 준호_ID = UUID.fromString("8b9b67fd-1d88-4b30-bfea-cd8f89fc10d9"); + public static final UUID 지수_ID = UUID.fromString("de3b60d2-5672-464d-8769-bf5c9de5eaff"); + public static final UUID 혁진_ID = UUID.fromString("42880aaf-4b97-4b0c-8a8a-72df4bb592f6"); public static CrewCreateRequest createCrewRequest(UUID memberId, String title, String description, int maxMembers) { return new CrewCreateRequest( @@ -26,4 +39,52 @@ public static List createMultipleCrews(int count, UUID member }) .collect(Collectors.toList()); } + + public static Crew createCrew(UUID leaderId) { + return Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + leaderId); + } + + public static Crew createCrewWithMembers(UUID leaderId) { + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + leaderId); + List crewMemberList = List.of( + new CrewMember(crew, leaderId, CrewMemberRole.LEADER), + new CrewMember(crew, 정수_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 홍석_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 준호_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 지수_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 혁진_ID, CrewMemberRole.PARTICIPANT) + ); + CrewMembers crewMembers = new CrewMembers(crew, leaderId); + ReflectionTestUtils.setField(crewMembers, "values", crewMemberList); + ReflectionTestUtils.setField(crew, "crewMembers", crewMembers); + return crew; + } + + public static Crew createCrewWithMembers(UUID leaderId, int maxMembers) { + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + maxMembers, + leaderId); + List crewMemberList = List.of( + new CrewMember(crew, leaderId, CrewMemberRole.LEADER), + new CrewMember(crew, 정수_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 홍석_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 준호_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 지수_ID, CrewMemberRole.PARTICIPANT), + new CrewMember(crew, 혁진_ID, CrewMemberRole.PARTICIPANT) + ); + CrewMembers crewMembers = new CrewMembers(crew, leaderId); + ReflectionTestUtils.setField(crewMembers, "values", crewMemberList); + ReflectionTestUtils.setField(crew, "crewMembers", crewMembers); + return crew; + } } diff --git a/src/test/java/com/retrip/crew/domain/entity/CrewMembersTest.java b/src/test/java/com/retrip/crew/domain/entity/CrewMembersTest.java new file mode 100644 index 0000000..b9f0c1d --- /dev/null +++ b/src/test/java/com/retrip/crew/domain/entity/CrewMembersTest.java @@ -0,0 +1,55 @@ +package com.retrip.crew.domain.entity; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static com.retrip.crew.common.fixture.CrewFixture.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class CrewMembersTest { + + @Test + void 사용자가_리더이면_true를_반환한다() { + // given + Crew crew = createCrewWithMembers(MEMBER_ID); + + // when + boolean result = crew.getCrewMembers().isLeader(MEMBER_ID); + + // then + assertThat(result).isTrue(); + } + + @Test + void 사용자가_리더가_아니면_false를_반환한다() { + // given + Crew crew = createCrewWithMembers(LEADER_ID); + + // when + boolean result = crew.getCrewMembers().isLeader(정수_ID); + + // then + assertThat(result).isFalse(); + } + + @Test + void 사용자를_크루_멤버에_추가한다() { + // given + Crew crew = createCrew(LEADER_ID); + Demand demand = new Demand(정수_ID, crew); + CrewMembers crewMembers = new CrewMembers(crew, LEADER_ID); + + // when + crewMembers.addMember(demand, crew); + + // then + List ids = crewMembers.getValues().stream().map(CrewMember::getMemberId).toList(); + assertAll( + () -> assertThat(crewMembers.getSize()).isEqualTo(2), + () -> assertThat(ids.contains(정수_ID)).isTrue() + ); + } +} diff --git a/src/test/java/com/retrip/crew/domain/entity/CrewTest.java b/src/test/java/com/retrip/crew/domain/entity/CrewTest.java index cb9ed60..37d2c90 100644 --- a/src/test/java/com/retrip/crew/domain/entity/CrewTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/CrewTest.java @@ -1,13 +1,10 @@ package com.retrip.crew.domain.entity; -import com.retrip.crew.domain.exception.common.IllegalStateException; +import com.retrip.crew.domain.exception.UnableToStartRecruitmentException; import com.retrip.crew.domain.vo.RecruitmentStatus; import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.List; -import java.util.UUID; +import static com.retrip.crew.common.fixture.CrewFixture.*; import static org.assertj.core.api.AssertionsForClassTypes.*; class CrewTest { @@ -17,18 +14,14 @@ class CrewTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - UUID.randomUUID()) + LEADER_ID) ).doesNotThrowAnyException(); } @Test void 크루가_생성되면_모집을_시작한다() { // given, when - Crew crew = Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100, - UUID.randomUUID()); + Crew crew = createCrew(LEADER_ID); // then assertThat(crew.getRecruitment().getStatus()).isEqualTo(RecruitmentStatus.RECRUITING); @@ -37,11 +30,7 @@ class CrewTest { @Test void 크루_모집을_중지한다() { // given - Crew crew = Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100, - UUID.randomUUID()); + Crew crew = createCrew(LEADER_ID); // when crew.stopRecruitment(); @@ -53,11 +42,7 @@ class CrewTest { @Test void 크루_모집을_시작한다() { // given - Crew crew = Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100, - UUID.randomUUID()); + Crew crew = createCrewWithMembers(LEADER_ID, 100); crew.stopRecruitment(); // when @@ -70,26 +55,12 @@ class CrewTest { @Test void 크루_멤버가_최대_모집_인원과_같으면_모집을_시작할_수_없다() { // given - Crew crew = Crew.create( - "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 5, - UUID.randomUUID()); - List crewMemberList = List.of( - new CrewMember(crew, UUID.randomUUID(), CrewMemberRole.LEADER), - new CrewMember(crew, UUID.randomUUID(), CrewMemberRole.PARTICIPANT), - new CrewMember(crew, UUID.randomUUID(), CrewMemberRole.PARTICIPANT), - new CrewMember(crew, UUID.randomUUID(), CrewMemberRole.PARTICIPANT), - new CrewMember(crew, UUID.randomUUID(), CrewMemberRole.PARTICIPANT) - ); + Crew crew = createCrewWithMembers(LEADER_ID, 6); crew.stopRecruitment(); - CrewMembers crewMembers = new CrewMembers(crew, UUID.randomUUID()); - ReflectionTestUtils.setField(crewMembers, "values", crewMemberList); - ReflectionTestUtils.setField(crew, "crewMembers", crewMembers); // when, then assertThatThrownBy(crew::startRecruitment) - .isExactlyInstanceOf(IllegalStateException.class); + .isExactlyInstanceOf(UnableToStartRecruitmentException.class); assertThat(crew.getRecruitment().getStatus()).isEqualTo(RecruitmentStatus.STOPPED); } } diff --git a/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java b/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java index 298ebf5..221221e 100644 --- a/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java @@ -1,8 +1,17 @@ package com.retrip.crew.domain.entity; +import com.retrip.crew.domain.exception.IllegalDemandStateException; +import com.retrip.crew.domain.vo.DemandStatus; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.test.util.ReflectionTestUtils; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import java.util.List; + +import static com.retrip.crew.common.fixture.CrewFixture.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; class RecruitmentTest { @Test @@ -10,4 +19,86 @@ class RecruitmentTest { assertThatCode(() -> new Recruitment(100)) .doesNotThrowAnyException(); } + + @Test + void 참여_요청을_취소한다() { + // given + Crew crew = createCrew(LEADER_ID); + Recruitment recruitment = new Recruitment(100); + Demand demand = new Demand(정수_ID, crew); + List demands = List.of( + demand, + new Demand(홍석_ID, crew), + new Demand(준호_ID, crew), + new Demand(지수_ID, crew), + new Demand(혁진_ID, crew) + ); + ReflectionTestUtils.setField(recruitment, "demands", demands); + + // when + recruitment.cancelDemand(demand); + + // then + assertThat(demand.getStatus()).isEqualTo(DemandStatus.CANCELED); + } + + @Test + void 취소한_참여_요청을_재요청_한다() { + // given + Crew crew = createCrew(LEADER_ID); + Recruitment recruitment = new Recruitment(100); + Demand demand = new Demand(정수_ID, crew); + List demands = List.of( + demand, + new Demand(홍석_ID, crew), + new Demand(준호_ID, crew), + new Demand(지수_ID, crew), + new Demand(혁진_ID, crew) + ); + ReflectionTestUtils.setField(demand, "status", DemandStatus.CANCELED); + ReflectionTestUtils.setField(recruitment, "demands", demands); + + // when + recruitment.addDemand(정수_ID, crew); + + // then + assertThat(demand.getStatus()).isEqualTo(DemandStatus.PENDING); + } + + @ParameterizedTest + @EnumSource(mode = EXCLUDE, names = {"PENDING"}) + void 대기_상태가_아니면_참여_요청을_취소할_수_있다(DemandStatus status) { + // given + Crew crew = createCrew(LEADER_ID); + Recruitment recruitment = new Recruitment(100); + Demand demand = new Demand(정수_ID, crew); + List demands = List.of( + demand, + new Demand(홍석_ID, crew), + new Demand(준호_ID, crew), + new Demand(지수_ID, crew), + new Demand(혁진_ID, crew) + ); + ReflectionTestUtils.setField(demand, "status", status); + ReflectionTestUtils.setField(recruitment, "demands", demands); + + // when, then + assertThatThrownBy(() -> recruitment.cancelDemand(demand)) + .isExactlyInstanceOf(IllegalDemandStateException.class); + } + + @Test + void 참여_요청을_승인한다() { + // given + Crew crew = createCrew(LEADER_ID); + Recruitment recruitment = new Recruitment(100); + Demand demand = new Demand(정수_ID, crew); + ReflectionTestUtils.setField(recruitment, "demands", List.of(demand)); + + // when + recruitment.approveDemand(demand); + + // then + assertThat(demand.getStatus()).isEqualTo(DemandStatus.APPROVED); + } } diff --git a/src/test/java/resources/application.yml b/src/test/resources/application.yml similarity index 89% rename from src/test/java/resources/application.yml rename to src/test/resources/application.yml index 1efe678..c5a6870 100644 --- a/src/test/java/resources/application.yml +++ b/src/test/resources/application.yml @@ -5,11 +5,9 @@ spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL - username: sa - password: jpa: hibernate: - ddl-auto: create-drop + ddl-auto: create properties: hibernate: show_sql: true @@ -17,9 +15,9 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect default_batch_fetch_size: 100 open-in-view: false - #logging logging: level: org.hibernate.type.descriptor.sql: trace + org.hibernate.orm.jdbc.bind: TRACE org.springframework.web.client.RestTemplate: DEBUG