diff --git a/build.gradle b/build.gradle index 7596bba..3c88f9e 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation "org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.5" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5" - implementation 'mysql:mysql-connector-java:8.0.33' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' diff --git a/src/main/java/com/retrip/crew/application/in/RecruitmentQuestionService.java b/src/main/java/com/retrip/crew/application/in/RecruitmentQuestionService.java new file mode 100644 index 0000000..91020f9 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/RecruitmentQuestionService.java @@ -0,0 +1,87 @@ +package com.retrip.crew.application.in; + +import com.retrip.crew.application.in.request.CreateRecruitmentQuestionRequest; +import com.retrip.crew.application.in.request.UpdateRecruitmentQuestionRequest; +import com.retrip.crew.application.in.response.CreateRecruitmentQuestionResponse; +import com.retrip.crew.application.in.response.RecruitmentQuestionResponse; +import com.retrip.crew.application.in.response.UpdateRecruitmentQuestionResponse; +import com.retrip.crew.application.in.usecase.GetRecruitmentQuestionUseCase; +import com.retrip.crew.application.in.usecase.ManageRecruitmentQuestionUseCase; +import com.retrip.crew.application.out.repository.CrewMemberQueryRepository; +import com.retrip.crew.application.out.repository.CrewQueryRepository; +import com.retrip.crew.application.out.repository.RecruitmentQuestionQueryRepository; +import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.entity.CrewMember; +import com.retrip.crew.domain.entity.RecruitmentQuestion; +import com.retrip.crew.domain.exception.CrewMemberNotFoundException; +import com.retrip.crew.domain.exception.CrewNotFoundException; +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.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@Service +@Transactional +@RequiredArgsConstructor +public class RecruitmentQuestionService implements ManageRecruitmentQuestionUseCase, GetRecruitmentQuestionUseCase { + + private final CrewQueryRepository crewQueryRepository; + private final CrewMemberQueryRepository crewMemberQueryRepository; + private final RecruitmentQuestionQueryRepository recruitmentQuestionQueryRepository; + + @Override + public CreateRecruitmentQuestionResponse createRecruitmentQuestion(UUID crewId, CreateRecruitmentQuestionRequest request) { + Crew crew = crewQueryRepository + .findByIdWithRecruitment(crewId) + .orElseThrow(CrewNotFoundException::new); + + RecruitmentQuestion question = RecruitmentQuestion.create(request.content(), crew); + crew.getRecruitment().addQuestion(question); + + return CreateRecruitmentQuestionResponse.of(question); + } + + @Override + public UpdateRecruitmentQuestionResponse updateRecruitmentQuestion(UUID crewId, UUID questionId, UpdateRecruitmentQuestionRequest request) { + Crew crew = crewQueryRepository + .findByIdWithRecruitment(crewId) + .orElseThrow(CrewNotFoundException::new); + + RecruitmentQuestion question = crew.getRecruitment() + .getRecruitmentQuestions() + .updateQuestion(questionId, request.content(), request.memberId()); + + return UpdateRecruitmentQuestionResponse.of(question); + } + + @Override + public void deleteRecruitmentQuestion(UUID crewId, UUID questionId, UUID memberId) { + Crew crew = crewQueryRepository + .findByIdWithRecruitment(crewId) + .orElseThrow(CrewNotFoundException::new); + + CrewMember crewMember = findCrewMemberByMemberId(crewId, memberId); + +// crew.getRecruitment() +// .getRecruitmentQuestions() +// .deleteQuestion(questionId, crewMember); + } + + private CrewMember findCrewMemberByMemberId(UUID crewId, UUID memberId) { + return crewMemberQueryRepository + .findCrewMemberByMemberId(crewId, memberId) + .orElseThrow(CrewMemberNotFoundException::new); + } + + @Override + @Transactional(readOnly = true) + public List getRecruitmentQuestions(UUID crewId) { + return recruitmentQuestionQueryRepository.findRecruitmentQuestions(crewId); + } +} 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/CreateRecruitmentQuestionRequest.java b/src/main/java/com/retrip/crew/application/in/request/CreateRecruitmentQuestionRequest.java new file mode 100644 index 0000000..e043ba5 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/CreateRecruitmentQuestionRequest.java @@ -0,0 +1,17 @@ +package com.retrip.crew.application.in.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.UUID; + +@Schema(description = "참여요청질문 생성 Request") +public record CreateRecruitmentQuestionRequest( + @Schema(description = "질문 내용") + @NotNull + @Size(min = 1, max = 100) + String content, + @Schema(description = "작성자") + @NotNull + UUID memberId +) {} diff --git a/src/main/java/com/retrip/crew/application/in/request/CrewOrder.java b/src/main/java/com/retrip/crew/application/in/request/CrewOrder.java new file mode 100644 index 0000000..4baabff --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/CrewOrder.java @@ -0,0 +1,12 @@ +package com.retrip.crew.application.in.request; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum CrewOrder { + DATE("createdAt"); + + private final String field; +} 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/request/UpdateRecruitmentQuestionRequest.java b/src/main/java/com/retrip/crew/application/in/request/UpdateRecruitmentQuestionRequest.java new file mode 100644 index 0000000..f87599a --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/UpdateRecruitmentQuestionRequest.java @@ -0,0 +1,17 @@ +package com.retrip.crew.application.in.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.UUID; + +@Schema(description = "참여요청질문 수정 Request") +public record UpdateRecruitmentQuestionRequest( + @Schema(description = "질문 내용") + @NotNull + @Size(min = 1, max = 100) + String content, + @Schema(description = "수정자") + @NotNull + UUID memberId +) {} diff --git a/src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java b/src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java index ce499e2..4a8770d 100644 --- a/src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java +++ b/src/main/java/com/retrip/crew/application/in/request/crew/CrewCreateRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Size; +import java.util.List; import java.util.UUID; @Schema(description = "크루 생성 Request") @@ -21,10 +22,14 @@ public record CrewCreateRequest( @Schema(description = "크루 최대 인원수") @Size(min = 5, max = 1000) - int maxMembers + int maxMembers, + + @Schema(description = "크루 질문 목록") + @Size(min = 1, max = 5) + List questions ){ public Crew to(UUID leader) { - return Crew.create(this.title, this.description, this.maxMembers, leader); + return Crew.create(this.title, this.description, this.maxMembers, leader,this.questions); } } 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..96dab2e --- /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/CreateRecruitmentQuestionResponse.java b/src/main/java/com/retrip/crew/application/in/response/CreateRecruitmentQuestionResponse.java new file mode 100644 index 0000000..040337b --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CreateRecruitmentQuestionResponse.java @@ -0,0 +1,19 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.RecruitmentQuestion; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "참여요청질문 생성 Response") +public record CreateRecruitmentQuestionResponse( + @Schema(description = "질문 ID") UUID id, + @Schema(description = "질문 내용") String content +) { + public static CreateRecruitmentQuestionResponse of(RecruitmentQuestion question) { + return new CreateRecruitmentQuestionResponse( + question.getId(), + question.getContent().getValue() + ); + } +} 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 new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..bc95740 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java @@ -0,0 +1,78 @@ +package com.retrip.crew.application.in.response; + +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; + +@Schema +@Builder +public record CrewDetailResponse( + @Schema(description = "크루 ID") + UUID id, + + @Schema(description = "크루 타이틀") + String title, + + @Schema(description = "크루 설명") + String description, + + @Schema(description = "크루 리더 ID") + UUID leaderId, + + @Schema(description = "크루원 현재 인원수") + Integer memberCount, + + @Schema(description = "크루원 수용가능한 최대 인원수") + Integer maxMemberCount, + + @Schema(description = "크루원 리스트") + List members +) { + + public static CrewDetailResponse of(Crew crew, int memberCount){ + return CrewDetailResponse.builder() + .id(crew.getId()) + .title(crew.getTitle().getValue()) + .description(crew.getDescription()) + .leaderId(crew.getLeader().getMemberId()) + .memberCount(memberCount) + .maxMemberCount(crew.getRecruitment().getMaxMembers()) + .members(toList(crew.getCrewMembers().getValues())) + .build(); + } + + public static List toList(List crewMembers){ + return crewMembers.stream() + .map(CrewMemberResponse::from) + .toList(); + } + + @Schema + public record CrewMemberResponse( + + @Schema(description = "크루 멤버 고유 ID") + UUID id, + + @Schema(description = "크루원 ID") + UUID memberId, + + @Schema(description = "크루원 역할코드") + String roleCode, + + @Schema(description = "크루원 역할명") + String roleName + ){ + public static CrewMemberResponse from(CrewMember crewMember){ + return new CrewMemberResponse( + crewMember.getId(), + crewMember.getMemberId(), + crewMember.getCrewMemberRole().getCode(), + crewMember.getCrewMemberRole().getViewName() + ); + } + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java new file mode 100644 index 0000000..7376a82 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java @@ -0,0 +1,23 @@ +package com.retrip.crew.application.in.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "크루 리스트 조회 Response") +public record CrewListResponse( + @Schema(description = "크루 ID") + UUID id, + + @Schema(description = "크루 타이틀") + String title, + + @Schema(description = "크루 리더 ID") + UUID leaderId, + + @Schema(description = "크루원 현재 인원수") + Long memberCount, + + @Schema(description = "크루원 수용가능한 최대 인원수") + Integer maxMemberCount +) { +} 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/response/RecruitmentQuestionResponse.java b/src/main/java/com/retrip/crew/application/in/response/RecruitmentQuestionResponse.java new file mode 100644 index 0000000..4171a56 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/RecruitmentQuestionResponse.java @@ -0,0 +1,10 @@ +package com.retrip.crew.application.in.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "참여요청질문 Response") +public record RecruitmentQuestionResponse( + @Schema(description = "질문 Id") UUID id, + @Schema(description = "질문 내용") String content +) {} diff --git a/src/main/java/com/retrip/crew/application/in/response/UpdateRecruitmentQuestionResponse.java b/src/main/java/com/retrip/crew/application/in/response/UpdateRecruitmentQuestionResponse.java new file mode 100644 index 0000000..5d8f9e5 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/UpdateRecruitmentQuestionResponse.java @@ -0,0 +1,19 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.RecruitmentQuestion; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema(description = "참여요청질문 수정 Response") +public record UpdateRecruitmentQuestionResponse( + @Schema(description = "질문 ID") UUID id, + @Schema(description = "질문 내용") String content +) { + public static UpdateRecruitmentQuestionResponse of(RecruitmentQuestion question) { + return new UpdateRecruitmentQuestionResponse( + question.getId(), + question.getContent().getValue() + ); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/usecase/GetRecruitmentQuestionUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/GetRecruitmentQuestionUseCase.java new file mode 100644 index 0000000..45277d9 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/GetRecruitmentQuestionUseCase.java @@ -0,0 +1,9 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.response.RecruitmentQuestionResponse; +import java.util.List; +import java.util.UUID; + +public interface GetRecruitmentQuestionUseCase { + List getRecruitmentQuestions(UUID crewId); +} \ No newline at end of file diff --git a/src/main/java/com/retrip/crew/application/in/usecase/ManageRecruitmentQuestionUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/ManageRecruitmentQuestionUseCase.java new file mode 100644 index 0000000..7a74319 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageRecruitmentQuestionUseCase.java @@ -0,0 +1,14 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.request.CreateRecruitmentQuestionRequest; +import com.retrip.crew.application.in.request.UpdateRecruitmentQuestionRequest; +import com.retrip.crew.application.in.response.CreateRecruitmentQuestionResponse; +import com.retrip.crew.application.in.response.UpdateRecruitmentQuestionResponse; + +import java.util.UUID; + +public interface ManageRecruitmentQuestionUseCase { + CreateRecruitmentQuestionResponse createRecruitmentQuestion(UUID crewId, CreateRecruitmentQuestionRequest request); + UpdateRecruitmentQuestionResponse updateRecruitmentQuestion(UUID crewId, UUID questionId, UpdateRecruitmentQuestionRequest request); + void deleteRecruitmentQuestion(UUID crewId, UUID questionId, UUID memberId); +} 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 bada9a6..1945997 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 @@ -14,5 +14,6 @@ public interface CrewQueryRepository { Slice getCrews(Pageable pageable, String keyword); Long getCrewCount(String keyword); Optional findByIdWithPosts(UUID id); + Optional findByIdWithRecruitment(UUID crewId); Page findAllContainsMember(Pageable pageable, UUID memberId); } diff --git a/src/main/java/com/retrip/crew/application/out/repository/RecruitmentQuestionQueryRepository.java b/src/main/java/com/retrip/crew/application/out/repository/RecruitmentQuestionQueryRepository.java new file mode 100644 index 0000000..b559073 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/out/repository/RecruitmentQuestionQueryRepository.java @@ -0,0 +1,12 @@ +package com.retrip.crew.application.out.repository; + +import com.retrip.crew.application.in.response.RecruitmentQuestionResponse; +import com.retrip.crew.domain.entity.RecruitmentQuestion; + +import java.util.List; +import java.util.UUID; + +public interface RecruitmentQuestionQueryRepository { + List findRecruitmentQuestions(UUID crewId); + RecruitmentQuestion findByIdOrElseThrow(UUID questionId); +} 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 e354fd6..9a85944 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Announcements.java +++ b/src/main/java/com/retrip/crew/domain/entity/Announcements.java @@ -3,7 +3,9 @@ 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 dca0285..c7c7bc4 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -13,6 +13,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.List; import java.util.UUID; @Entity @@ -39,19 +40,19 @@ public class Crew extends BaseEntity { @Embedded private Recruitment recruitment; - private Crew(String name, String description, int maxMembers, UUID leader) { + private Crew(String name, String description, int maxMembers, UUID leader, List questions) { this.id = UUID.randomUUID(); this.title = new CrewTitle(name); this.description = new CrewDescription(description); - this.recruitment = new Recruitment(maxMembers); + this.recruitment = Recruitment.of(maxMembers, questions, this); this.crewMembers = new CrewMembers(this, leader); this.posts = new Posts(); this.announcements = new Announcements(); this.introductions = new Introductions(); } - public static Crew create(String title, String description, int maxMembers, UUID leader) { - return new Crew(title, description, maxMembers, leader); + public static Crew create(String title, String description, int maxMembers, UUID leader, List questions) { + return new Crew(title, description, maxMembers, leader, questions); } public CrewMember getLeader() { diff --git a/src/main/java/com/retrip/crew/domain/entity/Introduction.java b/src/main/java/com/retrip/crew/domain/entity/Introduction.java index 0a10ce2..bc653d1 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Introduction.java +++ b/src/main/java/com/retrip/crew/domain/entity/Introduction.java @@ -11,7 +11,9 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; + import java.util.UUID; + import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -41,7 +43,7 @@ public class Introduction extends BaseEntity { ) private Crew crew; - public Introduction(UUID loginMemberId, String title, String content, Crew crew){ + public Introduction(UUID loginMemberId, String title, String content, Crew crew) { this.id = UUID.randomUUID(); this.memberId = loginMemberId; this.title = new IntroductionTitle(title); @@ -53,11 +55,11 @@ public static Introduction create(UUID loginMemberId, String title, String conte return new Introduction(loginMemberId, title, content, crew); } - public String getTitle(){ + public String getTitle() { return title.getValue(); } - public String getContent(){ + public String getContent() { return content.getValue(); } @@ -67,23 +69,23 @@ public void update(IntroductionTitle introductionTitle, IntroductionContent intr this.content = introductionContent; } - public void validateIntroductionOwner(UUID loginMemberId){ - if(!isOwner(loginMemberId)){ + public void validateIntroductionOwner(UUID loginMemberId) { + if (!isOwner(loginMemberId)) { throw new InvalidAccessException(); } } - public void validateIntroductionOwnerAndLeader(UUID loginMemberId){ - if(!isOwner(loginMemberId) || !isLeader(loginMemberId)){ + public void validateIntroductionOwnerAndLeader(UUID loginMemberId) { + if (!isOwner(loginMemberId) || !isLeader(loginMemberId)) { throw new InvalidAccessException(); } } - public boolean isOwner(UUID loginMemberId){ + public boolean isOwner(UUID loginMemberId) { return this.memberId.equals(loginMemberId); } - public boolean isLeader(UUID loginMemberId){ + public boolean isLeader(UUID loginMemberId) { return this.getCrew().getLeader().getMemberId().equals(loginMemberId); } diff --git a/src/main/java/com/retrip/crew/domain/entity/Introductions.java b/src/main/java/com/retrip/crew/domain/entity/Introductions.java index 7128b03..9ec712d 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Introductions.java +++ b/src/main/java/com/retrip/crew/domain/entity/Introductions.java @@ -3,8 +3,10 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Embeddable; import jakarta.persistence.OneToMany; + import java.util.ArrayList; import java.util.List; + import lombok.Getter; @Getter @@ -22,7 +24,7 @@ private List createEmptyValues() { return new ArrayList<>(); } - public void addIntroduction(Introduction introduction){ + public void addIntroduction(Introduction introduction) { this.values.add(introduction); } 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 50d14db..5fb8b27 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Recruitment.java +++ b/src/main/java/com/retrip/crew/domain/entity/Recruitment.java @@ -5,10 +5,7 @@ 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 jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -25,6 +22,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED, force = true) @Embeddable public class Recruitment { + private int maxMembers; @Column(name = "recruitment_status") @@ -33,9 +31,17 @@ public class Recruitment { @OneToMany(mappedBy = "crew", cascade = CascadeType.ALL, orphanRemoval = true) private List demands = new ArrayList<>(); - public Recruitment(int maxMembers) { + @Embedded + private RecruitmentQuestions recruitmentQuestions; + + private Recruitment(int maxMembers, List questions, Crew crew) { this.maxMembers = maxMembers; this.status = RECRUITING; + this.recruitmentQuestions = new RecruitmentQuestions(questions, crew); + } + + public static Recruitment of(int maxMembers, List questions, Crew crew) { + return new Recruitment(maxMembers, questions, crew); } public void start(int membersSize) { @@ -58,15 +64,21 @@ public void updateMaxMembers(int maxMembers) { this.maxMembers = maxMembers; } + public void addQuestion(RecruitmentQuestion question) { + this.recruitmentQuestions.add(question); + } + public Demand addDemand(UUID memberId, Crew crew) { if (isDuplicate(memberId)) { 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); return demand; @@ -84,8 +96,7 @@ private Optional findReDemand(UUID memberId) { } private static boolean isReDemand(UUID memberId, Demand demand) { - return demand.isEqualTo(memberId) - && demand.isCanceled(); + return demand.isEqualTo(memberId) && demand.isCanceled(); } public void cancelDemand(Demand demand) { @@ -106,6 +117,8 @@ public void rejectDemand(Demand demand) { find.reject(); } + + private static void throwIfNotPending(Demand find) { if (find.isNotPending()) { throw new IllegalDemandStateException("참여 요청의 상태가 대기중이 아닙니다."); diff --git a/src/main/java/com/retrip/crew/domain/entity/RecruitmentQuestion.java b/src/main/java/com/retrip/crew/domain/entity/RecruitmentQuestion.java new file mode 100644 index 0000000..6a43645 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/entity/RecruitmentQuestion.java @@ -0,0 +1,56 @@ +package com.retrip.crew.domain.entity; + +import com.retrip.crew.domain.exception.QuestionUpdateFailedException; +import com.retrip.crew.domain.vo.QuestionContent; +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 RecruitmentQuestion extends BaseEntity { + + @Id + @Column(columnDefinition = "varbinary(16)") + private UUID id; + + @Embedded + private QuestionContent content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "crew_id", + nullable = false, + columnDefinition = "varbinary(16)", + foreignKey = @ForeignKey(name = "fk_question_to_crew")) + private Crew crew; + + private RecruitmentQuestion(String content, Crew crew) { + this.id = UUID.randomUUID(); + this.content = new QuestionContent(content); + this.crew = crew; + } + + public static RecruitmentQuestion create(String content, Crew crew) { + return new RecruitmentQuestion(content, crew); + } + + public void update(String content, UUID memberId) { +// isUpdatable(this.getCreatedBy(), memberId); + this.content = new QuestionContent(content); + } + + private void isUpdatable(UUID createBy, UUID updateBy) { + if (!createBy.equals(updateBy)) { + throw new QuestionUpdateFailedException(); + } + } + +// public boolean isDeletable(CrewMember crewMember) { +// return crewMember.isLeader() || crewMember.isCreatedBy(this.getCreatedBy()); +// } +} diff --git a/src/main/java/com/retrip/crew/domain/entity/RecruitmentQuestions.java b/src/main/java/com/retrip/crew/domain/entity/RecruitmentQuestions.java new file mode 100644 index 0000000..840829f --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/entity/RecruitmentQuestions.java @@ -0,0 +1,85 @@ +package com.retrip.crew.domain.entity; + +import com.retrip.crew.domain.exception.QuestionDeleteFailedException; +import com.retrip.crew.domain.exception.QuestionNotFoundException; +import com.retrip.crew.domain.exception.common.ErrorCode; +import com.retrip.crew.domain.exception.common.InvalidValueException; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Getter +@Embeddable +public class RecruitmentQuestions { + + private static final int MAX_QUESTIONS = 10; + + @BatchSize(size = 100) + @OneToMany(mappedBy = "crew", cascade = CascadeType.ALL, orphanRemoval = true) + private List values = new ArrayList<>(); + + public RecruitmentQuestions() { + this.values = createEmptyValues(); + } + + public RecruitmentQuestions(List questions, Crew crew) { + validateQuestions(questions); + this.values = createEmptyValues(); + questions.forEach(questionText -> { + RecruitmentQuestion question = RecruitmentQuestion.create(questionText, crew); + this.values.add(question); + }); + } + + private List createEmptyValues() { + return new ArrayList<>(); + } + + private void validateQuestions(List values) { + if (values.size() > MAX_QUESTIONS) { + throw new InvalidValueException(ErrorCode.INVALID_QUESTION_COUNT); + } + } + + public RecruitmentQuestion updateQuestion(UUID questionId, String content, UUID memberId) { + RecruitmentQuestion question = findQuestion(questionId); + question.update(content, memberId); + return question; + } + +// public void deleteQuestion(UUID questionId, CrewMember crewMember) { +// RecruitmentQuestion question = findQuestion(questionId); +// if (!question.isDeletable(crewMember)) { +// throw new QuestionDeleteFailedException(); +// } +// this.values.remove(question); +// } + + private RecruitmentQuestion findQuestion(UUID questionId) { + return this.values.stream() + .filter(q -> q.getId().equals(questionId)) + .findAny() + .orElseThrow(QuestionNotFoundException::new); + } + + public void add(RecruitmentQuestion question) { + if (this.values.size() >= MAX_QUESTIONS) { + throw new InvalidValueException(ErrorCode.INVALID_QUESTION_COUNT); + } + this.values.add(question); + } + + public List getContents() { + return values.stream() + .map(q -> q.getContent().getValue()) + .toList(); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/QuestionDeleteFailedException.java b/src/main/java/com/retrip/crew/domain/exception/QuestionDeleteFailedException.java new file mode 100644 index 0000000..bd5b721 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/QuestionDeleteFailedException.java @@ -0,0 +1,10 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.BusinessException; +import com.retrip.crew.domain.exception.common.ErrorCode; + +public class QuestionDeleteFailedException extends BusinessException { + public QuestionDeleteFailedException() { + super(ErrorCode.QUESTION_DELETE_FAIL); + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/crew/domain/exception/QuestionNotFoundException.java b/src/main/java/com/retrip/crew/domain/exception/QuestionNotFoundException.java new file mode 100644 index 0000000..6a6c808 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/QuestionNotFoundException.java @@ -0,0 +1,10 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.BusinessException; +import com.retrip.crew.domain.exception.common.ErrorCode; + +public class QuestionNotFoundException extends BusinessException { + public QuestionNotFoundException() { + super(ErrorCode.QUESTION_NOT_FOUND); + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/crew/domain/exception/QuestionUpdateFailedException.java b/src/main/java/com/retrip/crew/domain/exception/QuestionUpdateFailedException.java new file mode 100644 index 0000000..f7fe24e --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/QuestionUpdateFailedException.java @@ -0,0 +1,12 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.BusinessException; +import com.retrip.crew.domain.exception.common.ErrorCode; + +public class QuestionUpdateFailedException extends BusinessException { + private static final ErrorCode errorCode = ErrorCode.QUESTION_UPDATE_FAIL; + + public QuestionUpdateFailedException() { + 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 1489011..79fca3f 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 @@ -14,6 +14,8 @@ public enum ErrorCode { ILLEGAL_STATE(BAD_REQUEST, "Common-005", "Illegal state"), CREW_NOT_FOUND(BAD_REQUEST, "Crew-001", "크루 엔티티를 찾을 수 없습니다."), + INVALID_QUESTION_COUNT(BAD_REQUEST, "Crew-002", "질문은 최대 10개까지 입력할 수 있습니다."), + INVALID_QUESTION_LENGTH(BAD_REQUEST, "Crew-003", "각 질문은 10자 이상 200자 이하로 입력해야 합니다."), NOT_CREW_LEADER(BAD_REQUEST, "Crew-002", "크루 리더가 아니면 접근할 수 없습니다."), UNABLE_TO_START_RECRUITMENT(BAD_REQUEST, "Crew-003", "크루 멤버를 모집할 수 없습니다."), DUPLICATE_DEMAND(BAD_REQUEST, "Crew-004", "이미 크루 참여를 요청한 사용자는 다시 요청할 수 없습니다."), @@ -24,7 +26,11 @@ public enum ErrorCode { CREW_MEMBER_NOT_FOUND(BAD_REQUEST, "Crew-008", "크루원 멤버 엔티티를 찾을 수 없습니다.."), POST_NOT_FOUND(BAD_REQUEST, "Crew-009", "자유 게시글 엔티티를 찾을 수 없습니다."), POST_UPDATE_FAIL(FORBIDDEN, "Crew-010", "자유 게시글을 수정할 권한이 없습니다."), - POST_DELETE_FAIL(FORBIDDEN, "Crew-011", "자유 게시글을 삭제할 권한이 없습니다.") + POST_DELETE_FAIL(FORBIDDEN, "Crew-011", "자유 게시글을 삭제할 권한이 없습니다."), + QUESTION_UPDATE_FAIL(FORBIDDEN, "Crew-012", "질문 수정 권한이 없습니다."), + QUESTION_DELETE_FAIL(FORBIDDEN, "Crew-013", "질문 삭제 권한이 없습니다."), + QUESTION_NOT_FOUND(BAD_REQUEST, "Crew-014", "질문을 찾을 수 없습니다."); + ; private final HttpStatus status; 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 c001268..22496e7 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 @@ -1,21 +1,21 @@ -package com.retrip.crew.domain.exception.common; + package com.retrip.crew.domain.exception.common; -public class InvalidValueException extends BusinessException { - private static final ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE; + public class InvalidValueException extends BusinessException { + private static final ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE; - public InvalidValueException() { - super(errorCode); - } - public InvalidValueException(ErrorCode errorCode) { - super(errorCode); - } + public InvalidValueException() { + super(errorCode); + } + public InvalidValueException(ErrorCode errorCode) { + super(errorCode); + } - public InvalidValueException(String message) { - super(errorCode, message); - } + public InvalidValueException(String message) { + super(errorCode, message); + } - public InvalidValueException(ErrorCode errorCode, String message) { - super(errorCode, message); - } + public InvalidValueException(ErrorCode errorCode, String message) { + super(errorCode, message); + } -} + } diff --git a/src/main/java/com/retrip/crew/domain/vo/QuestionContent.java b/src/main/java/com/retrip/crew/domain/vo/QuestionContent.java new file mode 100644 index 0000000..8017f73 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/vo/QuestionContent.java @@ -0,0 +1,34 @@ +package com.retrip.crew.domain.vo; + +import com.retrip.crew.domain.exception.common.ErrorCode; +import com.retrip.crew.domain.exception.common.InvalidValueException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true) +public class QuestionContent { + + private static final int MIN_LENGTH = 10; + private static final int MAX_LENGTH = 100; + + @Column(name = "content", nullable = false, length = MAX_LENGTH) + private final String value; + + public QuestionContent(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value == null || value.trim().length() < MIN_LENGTH || value.length() > MAX_LENGTH) { + throw new InvalidValueException(ErrorCode.INVALID_QUESTION_LENGTH); + } + } +} 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 22e8696..3dc9a6b 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 @@ -156,4 +156,15 @@ private OrderSpecifier createCrewOrderSpecifier(Pageable pageable) { } return new OrderSpecifier<>(Order.ASC, crew.createdAt); } + + @Override + public Optional findByIdWithRecruitment(UUID crewId) { + return Optional.ofNullable( + query.selectFrom(crew) + .distinct() + .leftJoin(crew.recruitment.recruitmentQuestions.values).fetchJoin() + .where(crew.id.eq(crewId)) + .fetchOne() + ); + } } diff --git a/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/RecruitmentQuestionQuerydslRepository.java b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/RecruitmentQuestionQuerydslRepository.java new file mode 100644 index 0000000..6e2dda9 --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/RecruitmentQuestionQuerydslRepository.java @@ -0,0 +1,50 @@ +package com.retrip.crew.infra.adapter.out.persistence.mysql.query; + +import static com.retrip.crew.domain.entity.QRecruitmentQuestion.recruitmentQuestion; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.retrip.crew.application.in.response.RecruitmentQuestionResponse; +import com.retrip.crew.application.out.repository.RecruitmentQuestionQueryRepository; +import com.retrip.crew.domain.entity.RecruitmentQuestion; +import com.retrip.crew.domain.exception.QuestionNotFoundException; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +@RequiredArgsConstructor +public class RecruitmentQuestionQuerydslRepository implements RecruitmentQuestionQueryRepository { + private final JPAQueryFactory query; + + @Override + public List findRecruitmentQuestions(UUID crewId) { + return query + .select(Projections.constructor( + RecruitmentQuestionResponse.class, + recruitmentQuestion.id, + recruitmentQuestion.content.value, + recruitmentQuestion.createdAt)) + .from(recruitmentQuestion) + .where(recruitmentQuestion.crew.id.eq(crewId)) + .orderBy(recruitmentQuestion.createdAt.desc()) + .fetch(); + } + + @Override + public RecruitmentQuestion findByIdOrElseThrow(UUID questionId) { + RecruitmentQuestion question = query + .selectFrom(recruitmentQuestion) + .where(recruitmentQuestion.id.eq(questionId)) + .fetchOne(); + + if (question == null) { + throw new QuestionNotFoundException(); + } + + return question; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a7d14fc..cb6bbcd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,15 +1,13 @@ spring: application: name: crew - #JPA +#JPA datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://retripstudydb.cv4sss0i0mk8.ap-southeast-2.rds.amazonaws.com:3306/crew?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: ${DB_USERNAME} - password: ${DB_PASSWORD} + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL jpa: hibernate: - ddl-auto: update + ddl-auto: create-drop properties: hibernate: show_sql: true 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 8f47f06..df96586 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -6,15 +6,18 @@ 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.response.IntroductionCreateResponse; import com.retrip.crew.application.in.response.IntroductionDetailResponse; 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.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.entity.Introduction; import com.retrip.crew.domain.exception.common.InvalidAccessException; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; @@ -27,6 +30,7 @@ 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; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -38,7 +42,9 @@ class CrewServiceTest extends ServiceTest { LEADER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 100 + 100, + createDefaultQuestions() + ); //when @@ -70,14 +76,54 @@ class CrewServiceTest extends ServiceTest { } @Test - void 크루를_검색_및_정렬_필터링하여_조회한다() { + void 크루_참여_요청을_생성한다() { + Crew crew = crewRepository.save(Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID, + createDefaultQuestions() + )); + CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID); + + CreateDemandResponse response = demandService.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, + List.of("질문1") + ); + crew.demand(MEMBER_ID); + crew.demand(UUID.randomUUID()); + crew.demand(UUID.randomUUID()); + Crew save = crewRepository.save(crew); + CreateDemandRequest request = new CreateDemandRequest(MEMBER_ID); + + assertThatThrownBy(() -> demandService.createDemand(save.getId(), request)) + .isExactlyInstanceOf(IllegalStateException.class); + } + + @Test + void 크루를_검색_및_정렬_필터링하여_조회한다(){ //given List requests = createMultipleCrews( 10, MEMBER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 5 + 5, + List.of("질문1") ); requests.forEach(request -> { CrewCreateResponse response = crewService.createCrew(request); @@ -100,13 +146,14 @@ class CrewServiceTest extends ServiceTest { } @Test - void 크루_상세를_조회한다() { + void 크루_상세를_조회한다(){ //given CrewCreateRequest request = createCrewRequest( MEMBER_ID, "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", - 5 + 5, + List.of("질문1") ); UUID crewId = crewService.createCrew(request).id(); @@ -130,7 +177,8 @@ class CrewServiceTest extends ServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID + MEMBER_ID, + createDefaultQuestions() )); IntroductionCreateRequest request = new IntroductionCreateRequest(MEMBER_ID, "정수의 자기소개!", "안녕하세요!"); @@ -148,7 +196,8 @@ class CrewServiceTest extends ServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID + MEMBER_ID, + createDefaultQuestions() ); Introduction introduction = Introduction.create( MEMBER_ID, @@ -177,7 +226,8 @@ class CrewServiceTest extends ServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID + MEMBER_ID, + createDefaultQuestions() ); Introduction introduction = Introduction.create( MEMBER_ID, @@ -202,7 +252,8 @@ class CrewServiceTest extends ServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID + MEMBER_ID, + createDefaultQuestions() ); Introduction introduction = Introduction.create( MEMBER_ID, @@ -229,7 +280,8 @@ class CrewServiceTest extends ServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID + MEMBER_ID, + createDefaultQuestions() ); Introduction introduction = Introduction.create( MEMBER_ID, diff --git a/src/test/java/com/retrip/crew/application/in/PostServiceTest.java b/src/test/java/com/retrip/crew/application/in/PostServiceTest.java index 185ee67..4b767a6 100644 --- a/src/test/java/com/retrip/crew/application/in/PostServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/PostServiceTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; +import static com.retrip.crew.common.fixture.CrewFixture.createDefaultQuestions; import static com.retrip.crew.common.fixture.PostFixture.MEMBER_ID; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -30,7 +31,9 @@ class PostServiceTest extends BasePostServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID)); + MEMBER_ID, + createDefaultQuestions() + )); postService.createPost( crew.getId(), PostFixture.createRequest("속초 여행 어디가 좋아요?", "친구들과 속초 여행 가려고 합니다. 속초 여행지 추천해 주세요~")); @@ -67,7 +70,8 @@ class PostServiceTest extends BasePostServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID)); + MEMBER_ID, + createDefaultQuestions())); crewRepository.save(crew); CreatePostRequest request = @@ -91,7 +95,8 @@ class PostServiceTest extends BasePostServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID)); + MEMBER_ID, + createDefaultQuestions())); CreatePostResponse createPostResponse = postService.createPost( crew.getId(), @@ -120,7 +125,8 @@ class PostServiceTest extends BasePostServiceTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - MEMBER_ID)); + MEMBER_ID, + createDefaultQuestions())); CreatePostResponse createPostResponse = postService.createPost( crew.getId(), 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 7c59f4c..1199e8a 100644 --- a/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java +++ b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java @@ -23,21 +23,32 @@ public abstract class CrewFixture { 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) { + public static List createDefaultQuestions() { + return List.of( + "크루에 지원한 이유는 무엇인가요?", + "어떤 활동을 기대하고 있나요?", + "자신을 한 문장으로 표현한다면?", + "크루에서 어떤 역할을 하고 싶나요?", + "추가로 하고 싶은 말이 있나요?" + ); + } + + public static CrewCreateRequest createCrewRequest(UUID memberId, String title, String description, int maxMembers, List questions) { return new CrewCreateRequest( memberId, title, description, - maxMembers + maxMembers, + questions ); } - public static List createMultipleCrews(int count, UUID memberId, String baseTitle, String baseDescription, int maxMembers) { + public static List createMultipleCrews(int count, UUID memberId, String baseTitle, String baseDescription, int maxMembers, List questions) { return IntStream.range(0, count) .mapToObj(i -> { String title = baseTitle + " " + (i + 1); String description = baseDescription + " " + (i + 1); - return createCrewRequest(memberId, title, description, maxMembers); + return createCrewRequest(memberId, title, description, maxMembers, questions); }) .collect(Collectors.toList()); } @@ -47,7 +58,8 @@ public static Crew createCrew(UUID leaderId) { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - leaderId); + leaderId, + createDefaultQuestions()); } public static Crew createCrewWithMutableMembers(UUID leaderId) { @@ -55,7 +67,8 @@ public static Crew createCrewWithMutableMembers(UUID leaderId) { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - leaderId); + leaderId, + createDefaultQuestions()); List crewMemberList = new ArrayList<>(5); crewMemberList.add(new CrewMember(crew, leaderId, CrewMemberRole.LEADER)); crewMemberList.add(new CrewMember(crew, 정수_ID, CrewMemberRole.PARTICIPANT)); @@ -74,7 +87,8 @@ public static Crew createCrewWithMembers(UUID leaderId) { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - leaderId); + leaderId, + createDefaultQuestions()); List crewMemberList = List.of( new CrewMember(crew, leaderId, CrewMemberRole.LEADER), new CrewMember(crew, 정수_ID, CrewMemberRole.PARTICIPANT), @@ -94,7 +108,8 @@ public static Crew createCrewWithMembers(UUID leaderId, int maxMembers) { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", maxMembers, - leaderId); + leaderId, + createDefaultQuestions()); List crewMemberList = List.of( new CrewMember(crew, leaderId, CrewMemberRole.LEADER), new CrewMember(crew, 정수_ID, CrewMemberRole.PARTICIPANT), 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 37d2c90..6df95f5 100644 --- a/src/test/java/com/retrip/crew/domain/entity/CrewTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/CrewTest.java @@ -4,6 +4,8 @@ import com.retrip.crew.domain.vo.RecruitmentStatus; import org.junit.jupiter.api.Test; +import java.util.List; + import static com.retrip.crew.common.fixture.CrewFixture.*; import static org.assertj.core.api.AssertionsForClassTypes.*; @@ -14,7 +16,8 @@ class CrewTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 100, - LEADER_ID) + LEADER_ID, + List.of("질문1")) ).doesNotThrowAnyException(); } diff --git a/src/test/java/com/retrip/crew/domain/entity/PostTest.java b/src/test/java/com/retrip/crew/domain/entity/PostTest.java index ec25d6c..a49f854 100644 --- a/src/test/java/com/retrip/crew/domain/entity/PostTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/PostTest.java @@ -8,6 +8,7 @@ import java.util.UUID; +import static com.retrip.crew.common.fixture.CrewFixture.createDefaultQuestions; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.*; @@ -19,7 +20,8 @@ class PostTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 5, - UUID.randomUUID()); + UUID.randomUUID(), + createDefaultQuestions()); //then @@ -42,7 +44,8 @@ class PostTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 5, - UUID.randomUUID()); + UUID.randomUUID(), + createDefaultQuestions()); //then @@ -61,7 +64,8 @@ class PostTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 5, - UUID.randomUUID()); + UUID.randomUUID(), + createDefaultQuestions()); Post post = Post.create( "속초 여행 어디가 좋아요?", "친구들과 속초 여행 가려고 합니다. 속초 여행지 추천해 주세요~", @@ -80,7 +84,8 @@ class PostTest { "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", 5, - UUID.randomUUID()); + UUID.randomUUID(), + createDefaultQuestions()); Post post = Post.create( "속초 여행 어디가 좋아요?", "친구들과 속초 여행 가려고 합니다. 속초 여행지 추천해 주세요~", 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 221221e..7c99d2c 100644 --- a/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/RecruitmentTest.java @@ -1,6 +1,7 @@ package com.retrip.crew.domain.entity; import com.retrip.crew.domain.exception.IllegalDemandStateException; +import com.retrip.crew.domain.exception.common.InvalidValueException; import com.retrip.crew.domain.vo.DemandStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -10,13 +11,15 @@ import java.util.List; 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.assertj.core.api.AssertionsForClassTypes.*; import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; class RecruitmentTest { @Test void 모집을_생성한다() { - assertThatCode(() -> new Recruitment(100)) + assertThatCode(() -> new Recruitment(100,)) .doesNotThrowAnyException(); } @@ -101,4 +104,41 @@ class RecruitmentTest { // then assertThat(demand.getStatus()).isEqualTo(DemandStatus.APPROVED); } + + @Test + void 질문이_10개를_넘으면_예외가_발생한다() { + + List questions = List.of( + "1","2","3","4","5","6","7","8","9","10","11" + ); + + // when, then + assertThatThrownBy(() -> new Recruitment(10, questions)) + .isInstanceOf(InvalidValueException.class) + .hasMessageContaining("질문"); + } + + @Test + void 질문_내용이_100자를_넘으면_예외가_발생한다() { + String longQuestion = "a".repeat(101); + List questions = List.of(longQuestion); + + assertThatThrownBy(() -> new Recruitment(10, questions)) + .isInstanceOf(InvalidValueException.class) + .hasMessageContaining("질문 내용은 100자를 넘을 수 없습니다."); + } + + + @Test + void 질문_목록을_문자열로_조회할_수_있다() { + // given + List questions = List.of("가입 이유는?", "이전에 크루 활동을 해본 경험이 있나요?"); + Recruitment recruitment = new Recruitment(10, questions); + + // when + List result = recruitment.getRecruitmentQuestions(); + + // then + assertThat(result).containsExactlyElementsOf(questions); + } } diff --git a/src/test/java/resources/application.yml b/src/test/java/resources/application.yml new file mode 100644 index 0000000..1efe678 --- /dev/null +++ b/src/test/java/resources/application.yml @@ -0,0 +1,25 @@ +spring: + application: + name: crew + #JPA + 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 + properties: + hibernate: + show_sql: true + format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect + default_batch_fetch_size: 100 + open-in-view: false + +#logging +logging: + level: + org.hibernate.type.descriptor.sql: trace + org.springframework.web.client.RestTemplate: DEBUG