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 661c355..0f56285 100644 --- a/src/main/java/com/retrip/crew/application/in/CrewService.java +++ b/src/main/java/com/retrip/crew/application/in/CrewService.java @@ -1,19 +1,33 @@ package com.retrip.crew.application.in; 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.usecase.GetCrewUseCase; +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 jakarta.transaction.Transactional; +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 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; @Service @Transactional @RequiredArgsConstructor -public class CrewService implements CreateCrewUseCase { +public class CrewService implements CreateCrewUseCase, GetCrewUseCase { private final CrewRepository crewRepository; + private final CrewMemberRepository crewMemberRepository; + private final CrewQueryRepository crewQueryRepository; @Override public CrewCreateResponse createCrew(CrewCreateRequest request) { @@ -21,4 +35,26 @@ public CrewCreateResponse createCrew(CrewCreateRequest request) { return CrewCreateResponse.of(crew); } + + @Override + @Transactional(readOnly = true) + public ScrollPageResponse getCrews(Pageable pageable, String keyword, CrewOrder order, String sort) { + Pageable orderPageable = PaginationUtils.createPageRequest(pageable, order.getField(), sort); + Slice result = crewQueryRepository.getCrews(orderPageable, keyword); + Long totalCount = crewQueryRepository.getCrewCount(keyword); + return ScrollPageResponse.of(totalCount, result.hasNext(), result.getContent()); + } + + @Override + @Transactional(readOnly = true) + public CrewDetailResponse getCrewDetail(UUID crewId) { + Crew crew = findById(crewId); + int memberCount = crewMemberRepository.countByCrewId(crewId); + return CrewDetailResponse.of(crew, memberCount); + } + + private Crew findById(UUID crewId){ + return crewRepository.findById(crewId) + .orElseThrow(CrewNotFoundException::new); + } } 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 ec2afc5..0ee1e93 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,6 +2,7 @@ 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; @@ -15,10 +16,13 @@ public record CrewCreateRequest( String title, @Schema(description = "크루 설명") @Size(min = 1, max = 500) - String description + String description, + @Schema(description = "크루 최대 인원") + @Min(1) + int maxMembers ){ public Crew to(UUID leader) { - return Crew.create(this.title, this.description, leader); + return Crew.create(this.title, this.description, this.maxMembers, leader); } } 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/response/CrewCreateResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewCreateResponse.java index c325d4f..cdb54e9 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 @@ -20,7 +20,7 @@ public static CrewCreateResponse of(Crew crew) { return new CrewCreateResponse( crew.getId(), crew.getTitle().getValue(), - crew.getDescription().getValue(), + crew.getDescription(), crew.getLeader().getId() ); } 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..95d4705 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java @@ -0,0 +1,77 @@ +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 java.util.List; +import java.util.UUID; +import lombok.Builder; + +@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.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/usecase/GetCrewUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java new file mode 100644 index 0000000..34b9bc4 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java @@ -0,0 +1,14 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.request.CrewOrder; +import com.retrip.crew.application.in.response.CrewDetailResponse; +import com.retrip.crew.application.in.response.CrewListResponse; +import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import java.util.UUID; +import org.springframework.data.domain.Pageable; + +public interface GetCrewUseCase { + ScrollPageResponse getCrews(Pageable pageable, String keyword, CrewOrder order, String sort); + + CrewDetailResponse getCrewDetail(UUID crewId); +} diff --git a/src/main/java/com/retrip/crew/application/out/repository/CrewMemberRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewMemberRepository.java new file mode 100644 index 0000000..c4151cc --- /dev/null +++ b/src/main/java/com/retrip/crew/application/out/repository/CrewMemberRepository.java @@ -0,0 +1,9 @@ +package com.retrip.crew.application.out.repository; + +import com.retrip.crew.domain.entity.CrewMember; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CrewMemberRepository extends JpaRepository { + int countByCrewId(UUID crewId); +} diff --git a/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java new file mode 100644 index 0000000..6a1c089 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java @@ -0,0 +1,10 @@ +package com.retrip.crew.application.out.repository; + +import com.retrip.crew.application.in.response.CrewListResponse; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface CrewQueryRepository { + Slice getCrews(Pageable pageable, String keyword); + Long getCrewCount(String keyword); +} diff --git a/src/main/java/com/retrip/crew/application/out/repository/CrewRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewRepository.java index 54dedff..a709c5a 100644 --- a/src/main/java/com/retrip/crew/application/out/repository/CrewRepository.java +++ b/src/main/java/com/retrip/crew/application/out/repository/CrewRepository.java @@ -1,6 +1,5 @@ package com.retrip.crew.application.out.repository; - import com.retrip.crew.domain.entity.Crew; import org.springframework.data.jpa.repository.JpaRepository; 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 d2c9f28..f5dc7c9 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -10,25 +10,24 @@ import java.util.UUID; @Entity +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Crew extends BaseEntity { @Id @Column(columnDefinition = "varbinary(16)") - @Getter private UUID id; - @Getter @Embedded private CrewTitle title; @Embedded - @Getter private CrewDescription description; - @Embedded private CrewMembers crewMembers; + @Column(name = "max_members", nullable = false) + private int maxMembers; @Embedded private Posts posts; @@ -39,26 +38,30 @@ public class Crew extends BaseEntity { @Embedded private Introductions introductions; - @Version private long version; - private Crew(String name, String description, UUID leader) { + 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.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, UUID leader) { - return new Crew(title, description, leader); + public static Crew create(String title, String description, int maxMembers, UUID leader) { + return new Crew(title, description, maxMembers, leader); } public CrewMember getLeader() { return crewMembers.getLeader(); } + public String getDescription(){ + return description.getValue(); + } + } 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 1b71cfb..cea499d 100644 --- a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java +++ b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java @@ -1,10 +1,20 @@ package com.retrip.crew.infra.adapter.in.presentation.rest; 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.usecase.GetCrewUseCase; +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 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.*; @@ -17,6 +27,7 @@ @Tag(name = "Crew", description = "크루 서비스") public class CrewController { private final CreateCrewUseCase createCrewUseCase; + private final GetCrewUseCase getCrewUseCase; @PostMapping @Schema(description = "크루 생성") @@ -24,4 +35,25 @@ public ResponseEntity createCrew(@RequestBody CrewCreateRequ CrewCreateResponse crew = createCrewUseCase.createCrew(request); return ResponseEntity.created(URI.create("/crews/" + crew.id())).body(crew); } + + @GetMapping + @Schema(description = "크루 리스트 조회") + public ResponseEntity>> getCrews( + @RequestParam(name = "keyword", required = false) String keyword, + @RequestParam(name = "order", defaultValue = "DATE") CrewOrder order, + @RequestParam(name = "sort", defaultValue = "asc") String sort, + @PageableDefault(size = 10) Pageable pageable + ) { + ScrollPageResponse response = getCrewUseCase.getCrews(pageable, keyword, order, sort); + return ResponseEntity.ok().body(ApiResponse.ok(response)); + } + + @GetMapping("/{crewId}") + @Schema(description = "크루 상세 조회") + public ResponseEntity> getCrewDetail( + @PathVariable("crewId") UUID crewId + ) { + CrewDetailResponse response = getCrewUseCase.getCrewDetail(crewId); + return ResponseEntity.ok().body(ApiResponse.ok(response)); + } } diff --git a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ScrollPageResponse.java b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ScrollPageResponse.java new file mode 100644 index 0000000..3d40b8c --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ScrollPageResponse.java @@ -0,0 +1,18 @@ +package com.retrip.crew.infra.adapter.in.presentation.rest.common; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ScrollPageResponse { + private Long totalCount; + private boolean hasNext; + private List list; + + public static ScrollPageResponse of(Long totalCount, boolean hasNext, List list) { + return new ScrollPageResponse<>(totalCount, hasNext, list); + } +} 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 new file mode 100644 index 0000000..a88f942 --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java @@ -0,0 +1,88 @@ +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; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.retrip.crew.application.in.request.CrewOrder; +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; + +@Repository +@RequiredArgsConstructor +public class CrewQuerydslRepository implements CrewQueryRepository { + private final JPAQueryFactory query; + + @Override + public Slice getCrews(Pageable pageable, String keyword) { + QCrewMember subCrewMember = new QCrewMember("subCrewMember"); + NumberPath MemberCountAlias = Expressions.numberPath(Long.class, "memberCount"); + OrderSpecifier orderSpecifier = createCrewOrderSpecifier(pageable); + + List fetch = query.select(Projections.constructor(CrewListResponse.class, + crew.id.as("id"), + crew.title.value.as("title"), + crewMember.id.as("leaderId"), + ExpressionUtils.as( + select(subCrewMember.count()) + .from(subCrewMember) + .where(subCrewMember.crew.id.eq(crew.id)) ,MemberCountAlias), + crew.maxMembers.as("maxMemberCount") + ) + ) + .from(crew) + .join(crewMember).on(crewMember.crew.id.eq(crew.id)) + .where( + crewTitleContains(keyword) + ) + .limit(pageable.getPageSize() + 1) + .offset(pageable.getOffset()) + .orderBy(orderSpecifier) + .fetch(); + return checkEndPage(pageable, fetch); + } + + @Override + public Long getCrewCount(String keyword) { + return query.select(crew.count()) + .from(crew) + .where( + crewTitleContains(keyword) + ) + .fetchOne(); + } + + private BooleanExpression crewTitleContains(String title) { + return title == null ? null : crew.title.value.contains(title); + } + + private OrderSpecifier createCrewOrderSpecifier(Pageable pageable) { + if (!pageable.getSort().isEmpty()) { + for (Sort.Order order : pageable.getSort()) { + Order direction = order.isAscending() ? Order.ASC : Order.DESC; + if (CrewOrder.DATE.getField().equals(order.getProperty())) { + return new OrderSpecifier<>(direction, crew.createdAt); + } else { + return new OrderSpecifier<>(Order.ASC, crew.createdAt); + } + } + } + return new OrderSpecifier<>(Order.ASC, crew.createdAt); + } +} diff --git a/src/main/java/com/retrip/crew/infra/util/PaginationUtils.java b/src/main/java/com/retrip/crew/infra/util/PaginationUtils.java new file mode 100644 index 0000000..82875ce --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/util/PaginationUtils.java @@ -0,0 +1,28 @@ +package com.retrip.crew.infra.util; + +import java.util.List; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; + +public abstract class PaginationUtils { + + public static Pageable createPageRequest(Pageable pageable, String order, String sort) { + return PageRequest.of( + pageable.getPageNumber(), pageable.getPageSize(), + Sort.by(Sort.Direction.fromString(sort), order) + ); + } + + /** 무한스크롤 위해 hasNext 체크 및 제거하는 util */ + public static Slice checkEndPage(Pageable pageable, List results) { + boolean hasNext = false; + if (results.size() > pageable.getPageSize()) { + hasNext = true; + results.remove(pageable.getPageSize()); + } + return new SliceImpl<>(results, pageable, hasNext); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 42bce00..f9bb91b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,7 @@ spring: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect + default_batch_fetch_size: 100 open-in-view: false #logging logging: diff --git a/src/test/java/com/retrip/crew/CrewApplicationTests.java b/src/test/java/com/retrip/crew/CrewApplicationTests.java index 405f037..7ecfcc6 100644 --- a/src/test/java/com/retrip/crew/CrewApplicationTests.java +++ b/src/test/java/com/retrip/crew/CrewApplicationTests.java @@ -15,6 +15,7 @@ void create() { assertThatCode(() -> Crew.create( "인천 크루 모집", "인천 크루원을 모집합니다.\n 가입나이: 20~29살 \n 금지 사항\n - 연애 금지\n - 만남 당일 취소 금지\n - 사적 연락 금지", + 4, UUID.randomUUID() )).doesNotThrowAnyException(); } 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 df9bf29..994ac86 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -1,44 +1,95 @@ package com.retrip.crew.application.in; 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.out.repository.CrewRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -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 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.domain.entity.CrewMemberRole; +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 static com.retrip.crew.common.fixture.CrewFixture.createMultipleCrews; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -class CrewServiceTest { - @Autowired - CrewRepository crewRepository; - - CrewService crewService; - UUID memberId = UUID.randomUUID(); - - @BeforeEach - void setUp() { - crewService = new CrewService(crewRepository); - } +class CrewServiceTest extends BaseTest { - @DisplayName("크루를 생성 한다.") @Test - void createCrew() { - CrewCreateRequest request = new CrewCreateRequest( - memberId, + void 크루를_생성_한다() { + //given + CrewCreateRequest request = createCrew( + 정수_ID, "속초 크루원 구함", - "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다." + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 5 ); + + //when CrewCreateResponse response = crewService.createCrew(request); + //then assertThat(response.id()).isNotNull(); assertThat(response.leaderId()).isNotNull(); } + + @Test + void 크루를_검색_및_정렬_필터링하여_조회한다(){ + //given + List requests = createMultipleCrews( + 10, + 정수_ID, + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 5 + ); + requests.forEach(request -> { + CrewCreateResponse response = crewService.createCrew(request); + }); + Pageable pageable = PageRequest.of(0, 5); + + //when + ScrollPageResponse response = crewService.getCrews(pageable, "속초 크루원", CrewOrder.DATE, "desc"); + + //then + assertAll( + "크루 검색 및 정렬 검증", + () -> assertThat(response).isNotNull(), + () -> assertThat(response.getList()).hasSize(pageable.getPageSize()), + () -> assertThat(response.getList().getFirst().title()).isEqualTo("속초 크루원 구함 10"), + () -> assertThat(response.getList().getFirst().memberCount()).isEqualTo(1), + () -> assertThat(response.getList().getFirst().maxMemberCount()).isEqualTo(5), + () -> assertThat(response.isHasNext()).isTrue() + ); + } + + @Test + void 크루_상세를_조회한다(){ + //given + CrewCreateRequest request = createCrew( + 정수_ID, + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 5 + ); + UUID crewId = crewService.createCrew(request).id(); + + //when + CrewDetailResponse response = crewService.getCrewDetail(crewId); + + //then + assertAll( + () -> assertThat(response.id()).isEqualTo(crewId), + () -> assertThat(response.leaderId()).isEqualTo(정수_ID), + () -> assertThat(response.members().size()).isEqualTo(1), + () -> assertThat(response.members().getFirst().roleCode()).isEqualTo(CrewMemberRole.LEADER.getCode()), + () -> assertThat(response.members().getFirst().memberId()).isEqualTo(정수_ID) + ); + } } diff --git a/src/test/java/com/retrip/crew/common/BaseTest.java b/src/test/java/com/retrip/crew/common/BaseTest.java new file mode 100644 index 0000000..582349d --- /dev/null +++ b/src/test/java/com/retrip/crew/common/BaseTest.java @@ -0,0 +1,41 @@ +package com.retrip.crew.common; + +import com.retrip.crew.application.in.CrewService; +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.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; + +@DataJpaTest +@Import(QuerydslConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class BaseTest { + + @Autowired + protected CrewRepository crewRepository; + + @Autowired + protected CrewMemberRepository crewMemberRepository; + + @Autowired + protected CrewQueryRepository crewQueryRepository; + + 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"); + + @BeforeEach + void setUp() { + crewService = new CrewService(crewRepository, crewMemberRepository, crewQueryRepository); + } +} diff --git a/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java b/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java new file mode 100644 index 0000000..7331886 --- /dev/null +++ b/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java @@ -0,0 +1,27 @@ +package com.retrip.crew.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.retrip.crew.infra.adapter.out.persistence.mysql.query.CrewQuerydslRepository; +import jakarta.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@TestConfiguration +public class QuerydslConfig { + + @Autowired + EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } + + @Bean + public CrewQuerydslRepository crewQuerydslRepository(JPAQueryFactory jpaQueryFactory) { + return new CrewQuerydslRepository(jpaQueryFactory); + } +} diff --git a/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java new file mode 100644 index 0000000..2ed3b6a --- /dev/null +++ b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java @@ -0,0 +1,29 @@ +package com.retrip.crew.common.fixture; + +import com.retrip.crew.application.in.request.CrewCreateRequest; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public abstract class CrewFixture { + + public static CrewCreateRequest createCrew(UUID memberId, String title, String description, int maxMembers) { + return new CrewCreateRequest( + memberId, + title, + description, + maxMembers + ); + } + + public static List createMultipleCrews(int count, UUID memberId, String baseTitle, String baseDescription, int maxMembers) { + return IntStream.range(0, count) + .mapToObj(i -> { + String title = baseTitle + " " + (i + 1); + String description = baseDescription + " " + (i + 1); + return createCrew(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 2c89485..187e9a7 100644 --- a/src/test/java/com/retrip/crew/domain/entity/CrewTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/CrewTest.java @@ -8,13 +8,13 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; class CrewTest { - @DisplayName("크루 생성 테스트") + @Test - public void create() { + public void 크루_생성_테스트() { assertThatCode(() -> Crew.create( "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 4, UUID.randomUUID())).doesNotThrowAnyException(); } - } diff --git a/src/test/java/com/retrip/crew/domain/vo/CrewIntroductionTitleTest.java b/src/test/java/com/retrip/crew/domain/vo/CrewIntroductionTitleTest.java index 657c233..2b5c820 100644 --- a/src/test/java/com/retrip/crew/domain/vo/CrewIntroductionTitleTest.java +++ b/src/test/java/com/retrip/crew/domain/vo/CrewIntroductionTitleTest.java @@ -1,7 +1,6 @@ package com.retrip.crew.domain.vo; import com.retrip.crew.domain.exception.common.InvalidValueException; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -9,8 +8,7 @@ class CrewIntroductionTitleTest { @Test - @DisplayName("크루 이름은 30자 미만으로 설정해야한다.") - public void validateName() { + public void 크루_이름은_30자_미만으로_설정해야한다() { assertThatThrownBy(() -> new CrewTitle("통천, 회양, 평강, 이천, 김화, 철원, 양구, 인제, 고성, 강릉, 속초 등 크루 인원 모집합니다.")) .isExactlyInstanceOf(InvalidValueException.class); } 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