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 0f56285..ed978f0 100644 --- a/src/main/java/com/retrip/crew/application/in/CrewService.java +++ b/src/main/java/com/retrip/crew/application/in/CrewService.java @@ -1,30 +1,37 @@ 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.response.CrewCreateResponse; -import com.retrip.crew.application.in.response.CrewDetailResponse; -import com.retrip.crew.application.in.response.CrewListResponse; -import com.retrip.crew.application.in.usecase.CreateCrewUseCase; +import com.retrip.crew.application.in.request.CrewUpdateRequest; +import com.retrip.crew.application.in.response.*; 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.CrewMemberRepository; import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; import com.retrip.crew.domain.entity.Crew; +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.vo.CrewDescription; +import com.retrip.crew.domain.vo.CrewTitle; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; import com.retrip.crew.infra.util.PaginationUtils; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.UUID; + @Service @Transactional @RequiredArgsConstructor -public class CrewService implements CreateCrewUseCase, GetCrewUseCase { +public class CrewService implements ManageCrewUseCase, UpdateRecruitmentUseCase, ManageDemandUseCase, GetCrewUseCase { private final CrewRepository crewRepository; private final CrewMemberRepository crewMemberRepository; private final CrewQueryRepository crewQueryRepository; @@ -32,10 +39,42 @@ public class CrewService implements CreateCrewUseCase, GetCrewUseCase { @Override public CrewCreateResponse createCrew(CrewCreateRequest request) { Crew crew = crewRepository.save(request.to(request.leader())); - return CrewCreateResponse.of(crew); } + @Override + public CrewUpdateResponse updateCrew(UUID crewId, CrewUpdateRequest request) { + Crew crew = findById(crewId); + CrewTitle title = new CrewTitle(request.title()); + CrewDescription description = new CrewDescription(request.description()); + Recruitment recruitment = crew.getRecruitment(); + + crew.update(title, description); + recruitment.updateMaxMembers(request.maxMembers()); + return CrewUpdateResponse.of(crew); + } + + @Override + public ChangeRecruitmentStatusResponse startRecruitment(UUID crewId) { + Crew crew = findById(crewId); + crew.startRecruitment(); + return ChangeRecruitmentStatusResponse.of(crew); + } + + @Override + public ChangeRecruitmentStatusResponse stopRecruitment(UUID crewId) { + Crew crew = findById(crewId); + crew.stopRecruitment(); + return ChangeRecruitmentStatusResponse.of(crew); + } + + @Override + public CreateDemandResponse createDemand(UUID crewId, CreateDemandRequest request) { + Crew crew = findById(crewId); + Demand demand = crew.demand(request.memberId()); + return CreateDemandResponse.of(crew.getId(), demand); + } + @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 new file mode 100644 index 0000000..537f1ea --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/CreateDemandRequest.java @@ -0,0 +1,8 @@ +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/CrewCreateRequest.java index 0ee1e93..145bd3a 100644 --- a/src/main/java/com/retrip/crew/application/in/request/CrewCreateRequest.java +++ b/src/main/java/com/retrip/crew/application/in/request/CrewCreateRequest.java @@ -2,7 +2,6 @@ import com.retrip.crew.domain.entity.Crew; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Size; import java.util.UUID; @@ -11,14 +10,17 @@ public record CrewCreateRequest( @Schema(description = "리더 ID") UUID leader, + @Schema(description = "크루 타이틀") @Size(min = 1, max = 30) String title, + @Schema(description = "크루 설명") @Size(min = 1, max = 500) String description, - @Schema(description = "크루 최대 인원") - @Min(1) + + @Schema(description = "크루 최대 인원수") + @Size(min = 5, max = 1000) int maxMembers ){ diff --git a/src/main/java/com/retrip/crew/application/in/request/CrewUpdateRequest.java b/src/main/java/com/retrip/crew/application/in/request/CrewUpdateRequest.java new file mode 100644 index 0000000..9d5a509 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/CrewUpdateRequest.java @@ -0,0 +1,20 @@ +package com.retrip.crew.application.in.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; + +@Schema(description = "크루 정보 수정 Request") +public record CrewUpdateRequest( + @Schema(description = "크루 타이틀") + @Size(min = 1, max = 30) + String title, + + @Schema(description = "크루 설명") + @Size(min = 1, max = 500) + String description, + + @Schema(description = "크루 최대 인원수") + @Size(min = 5, max = 1000) + int maxMembers +) { +} diff --git a/src/main/java/com/retrip/crew/application/in/response/ChangeRecruitmentStatusResponse.java b/src/main/java/com/retrip/crew/application/in/response/ChangeRecruitmentStatusResponse.java new file mode 100644 index 0000000..0a56b73 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/ChangeRecruitmentStatusResponse.java @@ -0,0 +1,19 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.vo.RecruitmentStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +public record ChangeRecruitmentStatusResponse( + @Schema(description = "크루 ID") + UUID id, + + @Schema(description = "모집 상태") + RecruitmentStatus status +) { + public static ChangeRecruitmentStatusResponse of(Crew crew) { + return new ChangeRecruitmentStatusResponse(crew.getId(), crew.getRecruitment().getStatus()); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/CreateDemandResponse.java b/src/main/java/com/retrip/crew/application/in/response/CreateDemandResponse.java new file mode 100644 index 0000000..1c42d65 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CreateDemandResponse.java @@ -0,0 +1,14 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Demand; + +import java.util.UUID; + +public record CreateDemandResponse( + UUID crewId, + UUID memberId +) { + public static CreateDemandResponse of(UUID crewId, Demand demand) { + return new CreateDemandResponse(crewId, demand.getMemberId()); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java index cdb54e9..ad231f9 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java @@ -1,6 +1,7 @@ package com.retrip.crew.application.in.response; import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.vo.RecruitmentStatus; import io.swagger.v3.oas.annotations.media.Schema; import java.util.UUID; @@ -9,19 +10,32 @@ public record CrewCreateResponse( @Schema(description = "크루 ID") UUID id, + @Schema(description = "크루 타이틀") String title, + @Schema(description = "크루 설명") String description, + @Schema(description = "리더 ID") - UUID leaderId + UUID leaderId, + + @Schema(description = "크루 최대 인원수") + int maxMembers, + + @Schema(description = "모집 상태") + RecruitmentStatus status + + ) { public static CrewCreateResponse of(Crew crew) { return new CrewCreateResponse( crew.getId(), crew.getTitle().getValue(), crew.getDescription(), - crew.getLeader().getId() + crew.getLeader().getId(), + crew.getRecruitment().getMaxMembers(), + crew.getRecruitment().getStatus() ); } } diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java index 95d4705..bc95740 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java @@ -3,9 +3,10 @@ import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.entity.CrewMember; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + import java.util.List; import java.util.UUID; -import lombok.Builder; @Schema @Builder @@ -39,7 +40,7 @@ public static CrewDetailResponse of(Crew crew, int memberCount){ .description(crew.getDescription()) .leaderId(crew.getLeader().getMemberId()) .memberCount(memberCount) - .maxMemberCount(crew.getMaxMembers()) + .maxMemberCount(crew.getRecruitment().getMaxMembers()) .members(toList(crew.getCrewMembers().getValues())) .build(); } diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewUpdateResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewUpdateResponse.java new file mode 100644 index 0000000..2e513c6 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CrewUpdateResponse.java @@ -0,0 +1,38 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.vo.RecruitmentStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +public record CrewUpdateResponse( + @Schema(description = "크루 ID") + UUID id, + + @Schema(description = "크루 타이틀") + String title, + + @Schema(description = "크루 설명") + String description, + + @Schema(description = "리더 ID") + UUID leaderId, + + @Schema(description = "크루 최대 인원수") + int maxMembers, + + @Schema(description = "모집 상태") + RecruitmentStatus status +) { + public static CrewUpdateResponse of(Crew crew) { + return new CrewUpdateResponse( + crew.getId(), + crew.getTitle().getValue(), + crew.getDescription(), + crew.getLeader().getId(), + crew.getRecruitment().getMaxMembers(), + crew.getRecruitment().getStatus() + ); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/usecase/CreateCrewUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/CreateCrewUseCase.java deleted file mode 100644 index e581d24..0000000 --- a/src/main/java/com/retrip/crew/application/in/usecase/CreateCrewUseCase.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.retrip.crew.application.in.usecase; - -import com.retrip.crew.application.in.request.CrewCreateRequest; -import com.retrip.crew.application.in.response.CrewCreateResponse; - -public interface CreateCrewUseCase { - CrewCreateResponse createCrew(CrewCreateRequest createRequest); -} 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 new file mode 100644 index 0000000..b8fb910 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java @@ -0,0 +1,14 @@ +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 java.util.UUID; + +public interface ManageCrewUseCase { + CrewCreateResponse createCrew(CrewCreateRequest request); + + CrewUpdateResponse updateCrew(UUID crewId, CrewUpdateRequest request); +} 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 new file mode 100644 index 0000000..ae7008f --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageDemandUseCase.java @@ -0,0 +1,10 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.request.CreateDemandRequest; +import com.retrip.crew.application.in.response.CreateDemandResponse; + +import java.util.UUID; + +public interface ManageDemandUseCase { + CreateDemandResponse createDemand(UUID crewId, CreateDemandRequest request); +} 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 new file mode 100644 index 0000000..f96e831 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/UpdateRecruitmentUseCase.java @@ -0,0 +1,11 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.response.ChangeRecruitmentStatusResponse; + +import java.util.UUID; + +public interface UpdateRecruitmentUseCase { + ChangeRecruitmentStatusResponse startRecruitment(UUID crewId); + + ChangeRecruitmentStatusResponse stopRecruitment(UUID crewId); +} 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 f5dc7c9..fd2180e 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -10,13 +10,16 @@ import java.util.UUID; @Entity -@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter public class Crew extends BaseEntity { @Id @Column(columnDefinition = "varbinary(16)") private UUID id; + @Version + private long version; + @Embedded private CrewTitle title; @@ -26,9 +29,6 @@ public class Crew extends BaseEntity { @Embedded private CrewMembers crewMembers; - @Column(name = "max_members", nullable = false) - private int maxMembers; - @Embedded private Posts posts; @@ -38,14 +38,14 @@ public class Crew extends BaseEntity { @Embedded private Introductions introductions; - @Version - private long version; + @Embedded + private Recruitment recruitment; private Crew(String name, String description, int maxMembers, UUID leader) { this.id = UUID.randomUUID(); this.title = new CrewTitle(name); this.description = new CrewDescription(description); - this.maxMembers = maxMembers; + this.recruitment = new Recruitment(maxMembers); this.crewMembers = new CrewMembers(this, leader); this.posts = new Posts(); this.announcements = new Announcements(); @@ -60,6 +60,24 @@ public CrewMember getLeader() { return crewMembers.getLeader(); } + public void startRecruitment() { + int membersSize = crewMembers.getSize(); + this.recruitment.start(membersSize); + } + + public void stopRecruitment() { + this.recruitment.stop(); + } + + public void update(CrewTitle title, CrewDescription description) { + this.title = title; + this.description = description; + } + + public Demand demand(UUID memberId) { + return recruitment.addDemand(memberId, this); + } + public String getDescription(){ return description.getValue(); } 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 90feba1..4a358b1 100644 --- a/src/main/java/com/retrip/crew/domain/entity/CrewMembers.java +++ b/src/main/java/com/retrip/crew/domain/entity/CrewMembers.java @@ -27,6 +27,13 @@ private List createLeader(Crew crew, UUID memberId) { } public CrewMember getLeader() { - return this.values.stream().filter(it -> it.getCrewMemberRole() == CrewMemberRole.LEADER).findFirst().orElse(null); + return this.values.stream() + .filter(it -> it.getCrewMemberRole() == CrewMemberRole.LEADER) + .findFirst() + .orElse(null); + } + + public int getSize() { + return this.values.size(); } } diff --git a/src/main/java/com/retrip/crew/domain/entity/Demand.java b/src/main/java/com/retrip/crew/domain/entity/Demand.java new file mode 100644 index 0000000..8fae633 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/entity/Demand.java @@ -0,0 +1,33 @@ +package com.retrip.crew.domain.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true) +@Getter +public class Demand { + @Id + @Column(columnDefinition = "varbinary(16)") + private UUID id; + private UUID memberId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "crew_id", + nullable = false, + columnDefinition = "varbinary(16)", + foreignKey = @ForeignKey(name = "fk_demand_to_crew") + ) + private Crew crew; + + public Demand(UUID memberId, Crew crew) { + this.id = UUID.randomUUID(); + this.memberId = memberId; + this.crew = crew; + } +} diff --git a/src/main/java/com/retrip/crew/domain/entity/Recruitment.java b/src/main/java/com/retrip/crew/domain/entity/Recruitment.java new file mode 100644 index 0000000..e445277 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/entity/Recruitment.java @@ -0,0 +1,68 @@ +package com.retrip.crew.domain.entity; + +import com.retrip.crew.domain.exception.common.IllegalStateException; +import com.retrip.crew.domain.vo.RecruitmentStatus; +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; +import java.util.UUID; + +import static com.retrip.crew.domain.vo.RecruitmentStatus.RECRUITING; +import static com.retrip.crew.domain.vo.RecruitmentStatus.STOPPED; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true) +@Embeddable +public class Recruitment { + private int maxMembers; + private RecruitmentStatus status; + + @OneToMany(mappedBy = "crew", cascade = CascadeType.ALL, orphanRemoval = true) + private List demands = new ArrayList<>(); + + public Recruitment(int maxMembers) { + this.maxMembers = maxMembers; + this.status = RECRUITING; + } + + public void start(int membersSize) { + if (isRecruitmentComplete(membersSize)) { + stop(); + throw new IllegalStateException("최대 인원을 모두 모집 완료하여 더 이상 멤버를 모집할 수 없습니다."); + } + this.status = RECRUITING; + } + + public void stop() { + this.status = STOPPED; + } + + private boolean isRecruitmentComplete(int membersSize) { + return this.maxMembers == membersSize; + } + + public void updateMaxMembers(int maxMembers) { + this.maxMembers = maxMembers; + } + + public Demand addDemand(UUID memberId, Crew crew) { + if (isDuplicate(memberId)) { + throw new IllegalStateException("이미 요청한 사용자는 다시 요청할 수 없습니다."); + } + Demand demand = new Demand(memberId, crew); + demands.add(demand); + return demand; + } + + private boolean isDuplicate(UUID memberId) { + return demands.stream() + .map(Demand::getMemberId) + .anyMatch(id -> id.equals(memberId)); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/common/BusinessException.java b/src/main/java/com/retrip/crew/domain/exception/common/BusinessException.java index ea05e90..5b4711d 100644 --- a/src/main/java/com/retrip/crew/domain/exception/common/BusinessException.java +++ b/src/main/java/com/retrip/crew/domain/exception/common/BusinessException.java @@ -6,6 +6,11 @@ public class BusinessException extends RuntimeException { private final ErrorCode errorCode; + public BusinessException() { + super(ErrorCode.SERVER_ERROR.getMessage()); + this.errorCode = ErrorCode.SERVER_ERROR; + } + public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; diff --git a/src/main/java/com/retrip/crew/domain/exception/common/EntityNotFoundException.java b/src/main/java/com/retrip/crew/domain/exception/common/EntityNotFoundException.java index 2c6d127..359e1f9 100644 --- a/src/main/java/com/retrip/crew/domain/exception/common/EntityNotFoundException.java +++ b/src/main/java/com/retrip/crew/domain/exception/common/EntityNotFoundException.java @@ -2,6 +2,11 @@ public class EntityNotFoundException extends BusinessException { private static final ErrorCode errorCode = ErrorCode.ENTITY_NOT_FOUND; + + public EntityNotFoundException() { + super(errorCode); + } + public EntityNotFoundException(ErrorCode errorCode) { super(errorCode); } 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 520a72c..4d05d8e 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 @@ -11,6 +11,7 @@ public enum ErrorCode { INVALID_INPUT_VALUE(BAD_REQUEST, "Common-002", "Invalid input value"), HANDLE_ACCESS_DENIED(FORBIDDEN, "Common-003", "Access is denied"), 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", "크루 엔티티를 찾을 수 없습니다.") ; diff --git a/src/main/java/com/retrip/crew/domain/exception/common/IllegalStateException.java b/src/main/java/com/retrip/crew/domain/exception/common/IllegalStateException.java new file mode 100644 index 0000000..f7f1889 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/common/IllegalStateException.java @@ -0,0 +1,21 @@ +package com.retrip.crew.domain.exception.common; + +public class IllegalStateException extends BusinessException { + private static final ErrorCode errorCode = ErrorCode.ILLEGAL_STATE; + + public IllegalStateException() { + super(errorCode); + } + + public IllegalStateException(ErrorCode errorCode) { + super(errorCode); + } + + public IllegalStateException(String message) { + super(errorCode, message); + } + + public IllegalStateException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/common/InvalidValueException.java b/src/main/java/com/retrip/crew/domain/exception/common/InvalidValueException.java index 546de82..c001268 100644 --- a/src/main/java/com/retrip/crew/domain/exception/common/InvalidValueException.java +++ b/src/main/java/com/retrip/crew/domain/exception/common/InvalidValueException.java @@ -2,6 +2,10 @@ public class InvalidValueException extends BusinessException { private static final ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE; + + public InvalidValueException() { + super(errorCode); + } public InvalidValueException(ErrorCode errorCode) { super(errorCode); } diff --git a/src/main/java/com/retrip/crew/domain/vo/RecruitmentStatus.java b/src/main/java/com/retrip/crew/domain/vo/RecruitmentStatus.java new file mode 100644 index 0000000..7c11d80 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/vo/RecruitmentStatus.java @@ -0,0 +1,24 @@ +package com.retrip.crew.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum RecruitmentStatus { + RECRUITING("RECRUITING", "모집 중"), + STOPPED("RECRUITMENT_CLOSED", "모집 중지") + ; + + private final String code; + private final String viewName; + + public static RecruitmentStatus codeOf(String code) { + return Arrays.stream(RecruitmentStatus.values()) + .filter(tripStatus -> tripStatus.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 cea499d..573ff92 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,39 +1,72 @@ 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.response.CrewCreateResponse; -import com.retrip.crew.application.in.response.CrewDetailResponse; -import com.retrip.crew.application.in.response.CrewListResponse; -import com.retrip.crew.application.in.usecase.CreateCrewUseCase; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; +import com.retrip.crew.application.in.request.CrewUpdateRequest; +import com.retrip.crew.application.in.response.*; 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.infra.adapter.in.presentation.rest.common.ApiResponse; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; -import java.util.UUID; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; -import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URI; +import java.util.UUID; @RequiredArgsConstructor @RequestMapping("/crews") @RestController @Tag(name = "Crew", description = "크루 서비스") public class CrewController { - private final CreateCrewUseCase createCrewUseCase; + private final ManageCrewUseCase manageCrewUseCase; + private final UpdateRecruitmentUseCase updateRecruitmentUseCase; + private final ManageDemandUseCase manageDemandUseCase; private final GetCrewUseCase getCrewUseCase; @PostMapping @Schema(description = "크루 생성") - public ResponseEntity createCrew(@RequestBody CrewCreateRequest request) { - CrewCreateResponse crew = createCrewUseCase.createCrew(request); - return ResponseEntity.created(URI.create("/crews/" + crew.id())).body(crew); + public ApiResponse createCrew(@RequestBody CrewCreateRequest request) { + CrewCreateResponse crew = manageCrewUseCase.createCrew(request); + return ApiResponse.created(crew); + } + + @PutMapping("/{crewId}") + @Schema(description = "크루 정보 수정") + public ApiResponse updateCrew( + @PathVariable UUID crewId, @RequestBody CrewUpdateRequest request) { + CrewUpdateResponse crew = manageCrewUseCase.updateCrew(crewId, request); + 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 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 a88f942..13e7a09 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 @@ -1,10 +1,5 @@ package com.retrip.crew.infra.adapter.out.persistence.mysql.query; -import static com.querydsl.jpa.JPAExpressions.select; -import static com.retrip.crew.domain.entity.QCrew.crew; -import static com.retrip.crew.domain.entity.QCrewMember.crewMember; -import static com.retrip.crew.infra.util.PaginationUtils.checkEndPage; - import com.querydsl.core.types.ExpressionUtils; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; @@ -17,13 +12,19 @@ import com.retrip.crew.application.in.response.CrewListResponse; import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.domain.entity.QCrewMember; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; +import java.util.List; + +import static com.querydsl.jpa.JPAExpressions.select; +import static com.retrip.crew.domain.entity.QCrew.crew; +import static com.retrip.crew.domain.entity.QCrewMember.crewMember; +import static com.retrip.crew.infra.util.PaginationUtils.checkEndPage; + @Repository @RequiredArgsConstructor public class CrewQuerydslRepository implements CrewQueryRepository { @@ -43,7 +44,7 @@ public Slice getCrews(Pageable pageable, String keyword) { select(subCrewMember.count()) .from(subCrewMember) .where(subCrewMember.crew.id.eq(crew.id)) ,MemberCountAlias), - crew.maxMembers.as("maxMemberCount") + crew.recruitment.maxMembers.as("maxMemberCount") ) ) .from(crew) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f9bb91b..cb6bbcd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,6 +19,7 @@ spring: logging: level: org.hibernate.type.descriptor.sql: trace + org.hibernate.orm.jdbc.bind: TRACE org.springframework.web.client.RestTemplate: DEBUG springdoc: diff --git a/src/test/java/com/retrip/crew/CrewApplicationTests.java b/src/test/java/com/retrip/crew/CrewApplicationTests.java index 7ecfcc6..f5ca28b 100644 --- a/src/test/java/com/retrip/crew/CrewApplicationTests.java +++ b/src/test/java/com/retrip/crew/CrewApplicationTests.java @@ -1,23 +1,8 @@ package com.retrip.crew; -import com.retrip.crew.domain.entity.Crew; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThatCode; - class CrewApplicationTests { - @DisplayName("제목을 입력해 크루를 생성할 수 있다.") @Test - void create() { - assertThatCode(() -> Crew.create( - "인천 크루 모집", - "인천 크루원을 모집합니다.\n 가입나이: 20~29살 \n 금지 사항\n - 연애 금지\n - 만남 당일 취소 금지\n - 사적 연락 금지", - 4, - UUID.randomUUID() - )).doesNotThrowAnyException(); - } - + void test() {} } 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 994ac86..a58c76a 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -1,34 +1,38 @@ 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.response.CrewCreateResponse; -import com.retrip.crew.application.in.response.CrewDetailResponse; -import com.retrip.crew.application.in.response.CrewListResponse; -import com.retrip.crew.common.BaseTest; +import com.retrip.crew.application.in.request.CrewUpdateRequest; +import com.retrip.crew.application.in.response.*; +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.infra.adapter.in.presentation.rest.common.ScrollPageResponse; -import java.util.List; -import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import static com.retrip.crew.common.fixture.CrewFixture.createCrew; +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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -class CrewServiceTest extends BaseTest { - +class CrewServiceTest extends ServiceTest { @Test void 크루를_생성_한다() { //given - CrewCreateRequest request = createCrew( - 정수_ID, + CrewCreateRequest request = createCrewRequest( + MEMBER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 5 + 100 ); //when @@ -39,12 +43,74 @@ class CrewServiceTest extends BaseTest { assertThat(response.leaderId()).isNotNull(); } + @Test + void 크루의_제목_설명_최대_인원수를_수정한다() { + // given + Crew crew = crewRepository.save(Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + )); + CrewUpdateRequest request = new CrewUpdateRequest( + "강릉 크루원 구함", + "강릉 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 200 + ); + + // when + CrewUpdateResponse response = crewService.updateCrew(crew.getId(), request); + + // then + assertAll( + () -> assertThat(response.title()).isEqualTo("강릉 크루원 구함"), + () -> assertThat(response.maxMembers()).isEqualTo(200) + ); + } + + @Test + void 크루_참여_요청을_생성한다() { + Crew crew = crewRepository.save(Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + )); + CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID); + + CreateDemandResponse response = crewService.createDemand(crew.getId(), request); + + List demands = crew.getRecruitment().getDemands(); + assertAll( + () -> assertThat(demands.size()).isEqualTo(1), + () -> assertThat(response.memberId()).isEqualTo(demands.get(0).getMemberId()) + ); + } + + @Test + void 이미_요청한_사용자는_다시_크루에_요청할_수_없다() { + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + ); + crew.demand(MEMBER_ID); + crew.demand(UUID.randomUUID()); + crew.demand(UUID.randomUUID()); + Crew save = crewRepository.save(crew); + CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID); + + assertThatThrownBy(() -> crewService.createDemand(save.getId(), request)) + .isExactlyInstanceOf(IllegalStateException.class); + } + @Test void 크루를_검색_및_정렬_필터링하여_조회한다(){ //given List requests = createMultipleCrews( 10, - 정수_ID, + MEMBER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 5 @@ -72,8 +138,8 @@ class CrewServiceTest extends BaseTest { @Test void 크루_상세를_조회한다(){ //given - CrewCreateRequest request = createCrew( - 정수_ID, + CrewCreateRequest request = createCrewRequest( + MEMBER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 5 @@ -86,10 +152,10 @@ class CrewServiceTest extends BaseTest { //then assertAll( () -> assertThat(response.id()).isEqualTo(crewId), - () -> assertThat(response.leaderId()).isEqualTo(정수_ID), + () -> assertThat(response.leaderId()).isEqualTo(MEMBER_ID), () -> assertThat(response.members().size()).isEqualTo(1), () -> assertThat(response.members().getFirst().roleCode()).isEqualTo(CrewMemberRole.LEADER.getCode()), - () -> assertThat(response.members().getFirst().memberId()).isEqualTo(정수_ID) + () -> assertThat(response.members().getFirst().memberId()).isEqualTo(MEMBER_ID) ); } } diff --git a/src/test/java/com/retrip/crew/common/BaseTest.java b/src/test/java/com/retrip/crew/common/ServiceTest.java similarity index 73% rename from src/test/java/com/retrip/crew/common/BaseTest.java rename to src/test/java/com/retrip/crew/common/ServiceTest.java index 582349d..a6452c3 100644 --- a/src/test/java/com/retrip/crew/common/BaseTest.java +++ b/src/test/java/com/retrip/crew/common/ServiceTest.java @@ -5,17 +5,18 @@ import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; import com.retrip.crew.common.config.QuerydslConfig; -import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 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) -public class BaseTest { +public class ServiceTest { @Autowired protected CrewRepository crewRepository; @@ -28,11 +29,7 @@ public class BaseTest { protected CrewService crewService; - protected UUID 정수_ID = UUID.fromString("13c8ab91-76bc-4f70-93e9-89f1a65dc640"); - protected UUID 홍석_ID = UUID.fromString("bf97d20b-d1f7-46a9-8362-11b9fa02d67d"); - protected UUID 준호_ID = UUID.fromString("8b9b67fd-1d88-4b30-bfea-cd8f89fc10d9"); - protected UUID 지수_ID = UUID.fromString("de3b60d2-5672-464d-8769-bf5c9de5eaff"); - protected UUID 혁진_ID = UUID.fromString("42880aaf-4b97-4b0c-8a8a-72df4bb592f6"); + protected UUID MEMBER_ID = UUID.fromString("13c8ab91-76bc-4f70-93e9-89f1a65dc640"); @BeforeEach void setUp() { 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 2ed3b6a..34ee232 100644 --- a/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java +++ b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java @@ -8,7 +8,7 @@ public abstract class CrewFixture { - public static CrewCreateRequest createCrew(UUID memberId, String title, String description, int maxMembers) { + public static CrewCreateRequest createCrewRequest(UUID memberId, String title, String description, int maxMembers) { return new CrewCreateRequest( memberId, title, @@ -22,7 +22,7 @@ public static List createMultipleCrews(int count, UUID member .mapToObj(i -> { String title = baseTitle + " " + (i + 1); String description = baseDescription + " " + (i + 1); - return createCrew(memberId, title, description, maxMembers); + return createCrewRequest(memberId, title, description, maxMembers); }) .collect(Collectors.toList()); } 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 187e9a7..cb9ed60 100644 --- a/src/test/java/com/retrip/crew/domain/entity/CrewTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/CrewTest.java @@ -1,20 +1,95 @@ package com.retrip.crew.domain.entity; -import org.junit.jupiter.api.DisplayName; +import com.retrip.crew.domain.exception.common.IllegalStateException; +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 org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.*; class CrewTest { - @Test - public void 크루_생성_테스트() { + void 크루를_생성한다() { assertThatCode(() -> Crew.create( "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 4, - UUID.randomUUID())).doesNotThrowAnyException(); + 100, + UUID.randomUUID()) + ).doesNotThrowAnyException(); + } + + @Test + void 크루가_생성되면_모집을_시작한다() { + // given, when + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + UUID.randomUUID()); + + // then + assertThat(crew.getRecruitment().getStatus()).isEqualTo(RecruitmentStatus.RECRUITING); + } + + @Test + void 크루_모집을_중지한다() { + // given + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + UUID.randomUUID()); + + // when + crew.stopRecruitment(); + + // then + assertThat(crew.getRecruitment().getStatus()).isEqualTo(RecruitmentStatus.STOPPED); + } + + @Test + void 크루_모집을_시작한다() { + // given + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + UUID.randomUUID()); + crew.stopRecruitment(); + + // when + crew.startRecruitment(); + + // then + assertThat(crew.getRecruitment().getStatus()).isEqualTo(RecruitmentStatus.RECRUITING); + } + + @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.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); + 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 new file mode 100644 index 0000000..298ebf5 --- /dev/null +++ b/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java @@ -0,0 +1,13 @@ +package com.retrip.crew.domain.entity; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +class RecruitmentTest { + @Test + void 모집을_생성한다() { + assertThatCode(() -> new Recruitment(100)) + .doesNotThrowAnyException(); + } +}