From 08acd934a36a703cdd7e84a6f8944f5b3dadd72d Mon Sep 17 00:00:00 2001 From: pparkjs Date: Sun, 2 Mar 2025 22:07:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[feat]=20=ED=81=AC=EB=A3=A8=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/application/in/CrewService.java | 19 +++- .../in/request/CrewCreateRequest.java | 9 +- .../application/in/request/CrewOrder.java | 12 +++ .../in/response/CrewListResponse.java | 13 +++ .../in/usecase/GetCrewUseCase.java | 10 +++ .../out/repository/CrewQueryRepository.java | 10 +++ .../com/retrip/crew/domain/entity/Crew.java | 9 +- .../in/presentation/rest/CrewController.java | 22 +++-- .../rest/common/ScrollPageResponse.java | 18 ++++ .../mysql/query/CrewQuerydslRepository.java | 86 +++++++++++++++++++ .../crew/infra/util/PaginationUtils.java | 28 ++++++ .../com/retrip/crew/CrewApplicationTests.java | 1 + .../crew/application/in/CrewServiceTest.java | 76 ++++++++++------ .../java/com/retrip/crew/common/BaseTest.java | 47 ++++++++++ .../retrip/crew/common/DatabaseCleaner.java | 26 ++++++ .../crew/common/helper/CrewTestHelper.java | 29 +++++++ .../retrip/crew/domain/entity/CrewTest.java | 1 + src/test/java/resources/application.yml | 24 ++++++ 18 files changed, 403 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/retrip/crew/application/in/request/CrewOrder.java create mode 100644 src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java create mode 100644 src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java create mode 100644 src/main/java/com/retrip/crew/application/out/repository/CrewQueryRepository.java create mode 100644 src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/common/ScrollPageResponse.java create mode 100644 src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java create mode 100644 src/main/java/com/retrip/crew/infra/util/PaginationUtils.java create mode 100644 src/test/java/com/retrip/crew/common/BaseTest.java create mode 100644 src/test/java/com/retrip/crew/common/DatabaseCleaner.java create mode 100644 src/test/java/com/retrip/crew/common/helper/CrewTestHelper.java create mode 100644 src/test/java/resources/application.yml 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..1303ec9 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,28 @@ 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.CrewListResponse; import com.retrip.crew.application.in.usecase.CreateCrewUseCase; +import com.retrip.crew.application.in.usecase.GetCrewUseCase; +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.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import com.retrip.crew.infra.util.PaginationUtils; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @Service @Transactional @RequiredArgsConstructor -public class CrewService implements CreateCrewUseCase { +public class CrewService implements CreateCrewUseCase, GetCrewUseCase { private final CrewRepository crewRepository; + private final CrewQueryRepository crewQueryRepository; @Override public CrewCreateResponse createCrew(CrewCreateRequest request) { @@ -21,4 +30,12 @@ public CrewCreateResponse createCrew(CrewCreateRequest request) { return CrewCreateResponse.of(crew); } + + @Override + 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()); + } } 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 c987d3d..f8f4a17 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 @@ -1,6 +1,8 @@ package com.retrip.crew.application.in.request; import com.retrip.crew.domain.entity.Crew; +import jakarta.validation.constraints.Min; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Size; import java.util.UUID; @@ -10,10 +12,13 @@ public record CrewCreateRequest( @Size(min = 1, max = 30) String title, @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/CrewListResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java new file mode 100644 index 0000000..e76de3b --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java @@ -0,0 +1,13 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Crew; +import java.util.UUID; + +public record CrewListResponse( + UUID id, + String title, + UUID leaderId, + Long memberCount, + 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..c23f801 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java @@ -0,0 +1,10 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.request.CrewOrder; +import com.retrip.crew.application.in.response.CrewListResponse; +import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import org.springframework.data.domain.Pageable; + +public interface GetCrewUseCase { + ScrollPageResponse getCrews(Pageable pageable, String keyword, CrewOrder order, String sort); +} 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/domain/entity/Crew.java b/src/main/java/com/retrip/crew/domain/entity/Crew.java index d2c9f28..72eea92 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -29,6 +29,8 @@ public class Crew extends BaseEntity { @Embedded private CrewMembers crewMembers; + @Column(name = "max_members", nullable = false) + private int maxMembers; @Embedded private Posts posts; @@ -43,18 +45,19 @@ public class Crew extends BaseEntity { @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() { 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 94d47bd..650e419 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,15 +1,15 @@ 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.CrewListResponse; import com.retrip.crew.application.in.usecase.CreateCrewUseCase; -import com.retrip.crew.domain.exception.CrewNotFoundException; -import com.retrip.crew.domain.exception.common.ErrorCode; -import com.retrip.crew.domain.exception.common.InvalidValueException; +import com.retrip.crew.application.in.usecase.GetCrewUseCase; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ApiResponse; -import jakarta.validation.Valid; +import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @@ -22,10 +22,22 @@ @RestController public class CrewController { private final CreateCrewUseCase createCrewUseCase; + private final GetCrewUseCase getCrewUseCase; @PostMapping public ResponseEntity createCrew(@RequestBody CrewCreateRequest request) { CrewCreateResponse crew = createCrewUseCase.createCrew(request); return ResponseEntity.created(URI.create("/crews/" + crew.id())).body(crew); } + + @GetMapping + 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)); + } } 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..d1ba6b1 --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/CrewQuerydslRepository.java @@ -0,0 +1,86 @@ +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.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; + return switch (order.getProperty()) { + case "createdAt" -> new OrderSpecifier<>(direction, crew.createdAt); + default -> 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/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..e05daff 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,68 @@ 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 com.retrip.crew.application.in.response.CrewListResponse; +import com.retrip.crew.common.BaseTest; +import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import java.util.List; 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 java.util.UUID; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import static com.retrip.crew.common.helper.CrewTestHelper.createCrew; +import static com.retrip.crew.common.helper.CrewTestHelper.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() + ); + } } 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..cc97ec1 --- /dev/null +++ b/src/test/java/com/retrip/crew/common/BaseTest.java @@ -0,0 +1,47 @@ +package com.retrip.crew.common; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import com.retrip.crew.application.in.CrewService; +import com.retrip.crew.application.out.repository.CrewQueryRepository; +import com.retrip.crew.application.out.repository.CrewRepository; +import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest +@TestInstance(PER_CLASS) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class BaseTest { + + private DatabaseCleaner databaseCleaner; + @Autowired + protected CrewRepository crewRepository; + + @Autowired + protected CrewQueryRepository crewQueryRepository; + + @Autowired + 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"); + + @BeforeAll + void beforeAll(@Autowired JdbcTemplate jdbcTemplate) { + this.databaseCleaner = new DatabaseCleaner(jdbcTemplate); + } + + @BeforeEach + void setUp() { + databaseCleaner.clean(); + } +} diff --git a/src/test/java/com/retrip/crew/common/DatabaseCleaner.java b/src/test/java/com/retrip/crew/common/DatabaseCleaner.java new file mode 100644 index 0000000..37805c0 --- /dev/null +++ b/src/test/java/com/retrip/crew/common/DatabaseCleaner.java @@ -0,0 +1,26 @@ +package com.retrip.crew.common; + +import java.util.List; +import org.springframework.jdbc.core.JdbcTemplate; + +public class DatabaseCleaner { + + private final JdbcTemplate jdbcTemplate; + private final List truncateQueries; + + public DatabaseCleaner(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.truncateQueries = jdbcTemplate.queryForList(""" + SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';') AS query + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'PUBLIC' + """, String.class + ); + } + + public void clean() { + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE"); + truncateQueries.forEach(jdbcTemplate::execute); + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE"); + } +} diff --git a/src/test/java/com/retrip/crew/common/helper/CrewTestHelper.java b/src/test/java/com/retrip/crew/common/helper/CrewTestHelper.java new file mode 100644 index 0000000..d854d8a --- /dev/null +++ b/src/test/java/com/retrip/crew/common/helper/CrewTestHelper.java @@ -0,0 +1,29 @@ +package com.retrip.crew.common.helper; + +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 CrewTestHelper { + + 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..11c368e 100644 --- a/src/test/java/com/retrip/crew/domain/entity/CrewTest.java +++ b/src/test/java/com/retrip/crew/domain/entity/CrewTest.java @@ -14,6 +14,7 @@ public void create() { assertThatCode(() -> Crew.create( "속초 크루원 구함", "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 4, UUID.randomUUID())).doesNotThrowAnyException(); } diff --git a/src/test/java/resources/application.yml b/src/test/java/resources/application.yml new file mode 100644 index 0000000..634d456 --- /dev/null +++ b/src/test/java/resources/application.yml @@ -0,0 +1,24 @@ +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 + open-in-view: false + +#logging +logging: + level: + org.hibernate.type.descriptor.sql: trace + org.springframework.web.client.RestTemplate: DEBUG From 534111373097dd9ebce428298df5caac8825457b Mon Sep 17 00:00:00 2001 From: pparkjs Date: Mon, 3 Mar 2025 11:16:40 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[feat]=20=ED=81=AC=EB=A3=A8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/application/in/CrewService.java | 19 ++++++- .../in/response/CrewDetailResponse.java | 53 +++++++++++++++++++ .../in/usecase/GetCrewUseCase.java | 4 ++ .../out/repository/CrewRepository.java | 1 + .../com/retrip/crew/domain/entity/Crew.java | 6 +-- .../in/presentation/rest/CrewController.java | 9 ++++ src/main/resources/application.yml | 1 + .../crew/application/in/CrewServiceTest.java | 27 ++++++++++ src/test/java/resources/application.yml | 1 + 9 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java 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 1303ec9..2bf63d6 100644 --- a/src/main/java/com/retrip/crew/application/in/CrewService.java +++ b/src/main/java/com/retrip/crew/application/in/CrewService.java @@ -3,19 +3,22 @@ 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.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; import com.retrip.crew.domain.entity.Crew; +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 jakarta.transaction.Transactional; +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 @@ -32,10 +35,24 @@ public CrewCreateResponse createCrew(CrewCreateRequest request) { } @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 = crewRepository.countById(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/response/CrewDetailResponse.java b/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java new file mode 100644 index 0000000..8271218 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/CrewDetailResponse.java @@ -0,0 +1,53 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.entity.CrewMember; +import java.util.List; +import java.util.UUID; +import lombok.Builder; + +@Builder +public record CrewDetailResponse( + UUID id, + String title, + String description, + UUID leaderId, + Integer memberCount, + Integer maxMemberCount, + List members +) { + + public static CrewDetailResponse of(Crew crew, int memberCount){ + return CrewDetailResponse.builder() + .id(crew.getId()) + .title(crew.getTitle().getValue()) + .description(crew.getDescription().getValue()) + .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(); + } + + public record CrewMemberResponse( + UUID id, + UUID memberId, + String roleCode, + 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/usecase/GetCrewUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java index c23f801..34b9bc4 100644 --- a/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java +++ b/src/main/java/com/retrip/crew/application/in/usecase/GetCrewUseCase.java @@ -1,10 +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/CrewRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewRepository.java index 54dedff..943a71d 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 @@ -7,4 +7,5 @@ import java.util.UUID; public interface CrewRepository extends JpaRepository { + int countById(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 72eea92..caf3e26 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -10,22 +10,19 @@ 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; @@ -41,7 +38,6 @@ public class Crew extends BaseEntity { @Embedded private Introductions introductions; - @Version private long version; 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 650e419..56006b4 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 @@ -3,6 +3,7 @@ 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; @@ -40,4 +41,12 @@ public ResponseEntity>> getCrew ScrollPageResponse response = getCrewUseCase.getCrews(pageable, keyword, order, sort); return ResponseEntity.ok().body(ApiResponse.ok(response)); } + + @GetMapping("/{crewId}") + public ResponseEntity> getCrewDetail( + @PathVariable("crewId") UUID crewId + ) { + CrewDetailResponse response = getCrewUseCase.getCrewDetail(crewId); + return ResponseEntity.ok().body(ApiResponse.ok(response)); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index aafba9a..4e63e42 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/application/in/CrewServiceTest.java b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java index e05daff..8640a0e 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -3,10 +3,13 @@ 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.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; @@ -65,4 +68,28 @@ class CrewServiceTest extends BaseTest { () -> 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/resources/application.yml b/src/test/java/resources/application.yml index 634d456..1efe678 100644 --- a/src/test/java/resources/application.yml +++ b/src/test/java/resources/application.yml @@ -15,6 +15,7 @@ spring: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect + default_batch_fetch_size: 100 open-in-view: false #logging From 094e96b8d70afe85c17f0a5076ee9d81748ee09a Mon Sep 17 00:00:00 2001 From: Kimjunho <39546306+xjvmdutl@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:06:21 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=9E=91=EC=97=85=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: junhokim --- build.gradle | 4 +++ .../in/request/CrewCreateRequest.java | 5 +++ .../in/response/CrewCreateResponse.java | 6 ++++ .../in/presentation/rest/CrewController.java | 4 +++ .../crew/infra/config/SwaggerConfig.java | 32 +++++++++++++++++++ src/main/resources/application.yml | 4 +++ 6 files changed, 55 insertions(+) create mode 100644 src/main/java/com/retrip/crew/infra/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 80325ee..3c88f9e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' 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" + + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' 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 f8f4a17..3c17927 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 @@ -1,16 +1,21 @@ package com.retrip.crew.application.in.request; import com.retrip.crew.domain.entity.Crew; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Size; import java.util.UUID; +@Schema(description = "크루 생성 Request") 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 = "크루 최대 인원") 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 b710eba..c325d4f 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,13 +1,19 @@ package com.retrip.crew.application.in.response; import com.retrip.crew.domain.entity.Crew; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.UUID; +@Schema(description = "크루 생성 Response") public record CrewCreateResponse( + @Schema(description = "크루 ID") UUID id, + @Schema(description = "크루 타이틀") String title, + @Schema(description = "크루 설명") String description, + @Schema(description = "리더 ID") UUID leaderId ) { public static CrewCreateResponse of(Crew crew) { 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 56006b4..f6184c7 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 @@ -6,6 +6,8 @@ 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; @@ -21,11 +23,13 @@ @RequiredArgsConstructor @RequestMapping("/crews") @RestController +@Tag(name = "Crew", description = "크루 서비스") public class CrewController { private final CreateCrewUseCase createCrewUseCase; 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); diff --git a/src/main/java/com/retrip/crew/infra/config/SwaggerConfig.java b/src/main/java/com/retrip/crew/infra/config/SwaggerConfig.java new file mode 100644 index 0000000..51ea12d --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/config/SwaggerConfig.java @@ -0,0 +1,32 @@ +package com.retrip.crew.infra.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + @Bean + public OpenAPI springShopOpenAPI() { + return new OpenAPI() + .addServersItem(new Server().url("/")) + .components( + new Components() + ).info( + new Info() + .title("Crew Application") + .version("v0.0.1") + ); + } + @Bean + public GroupedOpenApi crewApi(){ + return GroupedOpenApi.builder() + .group("crews") + .pathsToMatch("/crews/**") + .build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4e63e42..f9bb91b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,3 +20,7 @@ logging: level: org.hibernate.type.descriptor.sql: trace org.springframework.web.client.RestTemplate: DEBUG + +springdoc: + swagger-ui: + use-root-path: true From 938c5db6fd8a60d47a205de76d59e55ece0acec1 Mon Sep 17 00:00:00 2001 From: pparkjs Date: Sat, 15 Mar 2025 11:33:17 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[fix]=20=ED=81=AC=EB=A3=A8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/application/in/CrewService.java | 4 ++- .../in/response/CrewCreateResponse.java | 2 +- .../in/response/CrewDetailResponse.java | 26 ++++++++++++++++++- .../in/response/CrewListResponse.java | 12 ++++++++- .../out/repository/CrewMemberRepository.java | 9 +++++++ .../out/repository/CrewRepository.java | 2 -- .../com/retrip/crew/domain/entity/Crew.java | 4 +++ .../in/presentation/rest/CrewController.java | 2 ++ .../mysql/query/CrewQuerydslRepository.java | 10 ++++--- 9 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/retrip/crew/application/out/repository/CrewMemberRepository.java 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 2bf63d6..0f56285 100644 --- a/src/main/java/com/retrip/crew/application/in/CrewService.java +++ b/src/main/java/com/retrip/crew/application/in/CrewService.java @@ -7,6 +7,7 @@ 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; @@ -25,6 +26,7 @@ @RequiredArgsConstructor public class CrewService implements CreateCrewUseCase, GetCrewUseCase { private final CrewRepository crewRepository; + private final CrewMemberRepository crewMemberRepository; private final CrewQueryRepository crewQueryRepository; @Override @@ -47,7 +49,7 @@ public ScrollPageResponse getCrews(Pageable pageable, String k @Transactional(readOnly = true) public CrewDetailResponse getCrewDetail(UUID crewId) { Crew crew = findById(crewId); - int memberCount = crewRepository.countById(crewId); + int memberCount = crewMemberRepository.countByCrewId(crewId); return CrewDetailResponse.of(crew, memberCount); } 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 index 8271218..95d4705 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 @@ -2,18 +2,33 @@ 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 ) { @@ -21,7 +36,7 @@ public static CrewDetailResponse of(Crew crew, int memberCount){ return CrewDetailResponse.builder() .id(crew.getId()) .title(crew.getTitle().getValue()) - .description(crew.getDescription().getValue()) + .description(crew.getDescription()) .leaderId(crew.getLeader().getMemberId()) .memberCount(memberCount) .maxMemberCount(crew.getMaxMembers()) @@ -35,10 +50,19 @@ public static List toList(List crewMembers){ .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){ 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 index e76de3b..7376a82 100644 --- a/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java +++ b/src/main/java/com/retrip/crew/application/in/response/CrewListResponse.java @@ -1,13 +1,23 @@ package com.retrip.crew.application.in.response; -import com.retrip.crew.domain.entity.Crew; +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/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/CrewRepository.java b/src/main/java/com/retrip/crew/application/out/repository/CrewRepository.java index 943a71d..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,11 +1,9 @@ package com.retrip.crew.application.out.repository; - import com.retrip.crew.domain.entity.Crew; import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; public interface CrewRepository extends JpaRepository { - int countById(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 caf3e26..f5dc7c9 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -60,4 +60,8 @@ 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 f6184c7..cd4c85f 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 @@ -36,6 +36,7 @@ public ResponseEntity createCrew(@RequestBody CrewCreateRequ } @GetMapping + @Schema(description = "크루 리스트 조회") public ResponseEntity>> getCrews( @RequestParam(name = "keyword", required = false) String keyword, @RequestParam(name = "order", defaultValue = "DATE") CrewOrder order, @@ -47,6 +48,7 @@ public ResponseEntity>> getCrew } @GetMapping("/{crewId}") + @Schema(description = "크루 상세 조회") public ResponseEntity> getCrewDetail( @PathVariable("crewId") UUID crewId ) { 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 d1ba6b1..a88f942 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 @@ -13,6 +13,7 @@ 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; @@ -75,10 +76,11 @@ private OrderSpecifier createCrewOrderSpecifier(Pageable pageable) { if (!pageable.getSort().isEmpty()) { for (Sort.Order order : pageable.getSort()) { Order direction = order.isAscending() ? Order.ASC : Order.DESC; - return switch (order.getProperty()) { - case "createdAt" -> new OrderSpecifier<>(direction, crew.createdAt); - default -> new OrderSpecifier<>(Order.ASC, crew.createdAt); - }; + 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); From d3a480c3dff17c214a32da1d5234c98d2d1a8adc Mon Sep 17 00:00:00 2001 From: pparkjs Date: Sat, 15 Mar 2025 11:34:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[fix]=20=EB=8B=A8=EC=9C=84=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20SpringBootTest=20->=20DataJpaTest=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/application/in/CrewServiceTest.java | 4 +-- .../java/com/retrip/crew/common/BaseTest.java | 26 +++++++----------- .../retrip/crew/common/DatabaseCleaner.java | 26 ------------------ .../crew/common/config/QuerydslConfig.java | 27 +++++++++++++++++++ .../CrewFixture.java} | 4 +-- .../retrip/crew/domain/entity/CrewTest.java | 5 ++-- .../domain/vo/CrewIntroductionTitleTest.java | 4 +-- 7 files changed, 44 insertions(+), 52 deletions(-) delete mode 100644 src/test/java/com/retrip/crew/common/DatabaseCleaner.java create mode 100644 src/test/java/com/retrip/crew/common/config/QuerydslConfig.java rename src/test/java/com/retrip/crew/common/{helper/CrewTestHelper.java => fixture/CrewFixture.java} (92%) 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 8640a0e..994ac86 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -14,8 +14,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import static com.retrip.crew.common.helper.CrewTestHelper.createCrew; -import static com.retrip.crew.common.helper.CrewTestHelper.createMultipleCrews; +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; diff --git a/src/test/java/com/retrip/crew/common/BaseTest.java b/src/test/java/com/retrip/crew/common/BaseTest.java index cc97ec1..582349d 100644 --- a/src/test/java/com/retrip/crew/common/BaseTest.java +++ b/src/test/java/com/retrip/crew/common/BaseTest.java @@ -1,32 +1,31 @@ package com.retrip.crew.common; -import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; - 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.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; -@SpringBootTest -@TestInstance(PER_CLASS) +@DataJpaTest +@Import(QuerydslConfig.class) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class BaseTest { - private DatabaseCleaner databaseCleaner; @Autowired protected CrewRepository crewRepository; @Autowired - protected CrewQueryRepository crewQueryRepository; + protected CrewMemberRepository crewMemberRepository; @Autowired + protected CrewQueryRepository crewQueryRepository; + protected CrewService crewService; protected UUID 정수_ID = UUID.fromString("13c8ab91-76bc-4f70-93e9-89f1a65dc640"); @@ -35,13 +34,8 @@ public class BaseTest { protected UUID 지수_ID = UUID.fromString("de3b60d2-5672-464d-8769-bf5c9de5eaff"); protected UUID 혁진_ID = UUID.fromString("42880aaf-4b97-4b0c-8a8a-72df4bb592f6"); - @BeforeAll - void beforeAll(@Autowired JdbcTemplate jdbcTemplate) { - this.databaseCleaner = new DatabaseCleaner(jdbcTemplate); - } - @BeforeEach void setUp() { - databaseCleaner.clean(); + crewService = new CrewService(crewRepository, crewMemberRepository, crewQueryRepository); } } diff --git a/src/test/java/com/retrip/crew/common/DatabaseCleaner.java b/src/test/java/com/retrip/crew/common/DatabaseCleaner.java deleted file mode 100644 index 37805c0..0000000 --- a/src/test/java/com/retrip/crew/common/DatabaseCleaner.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.retrip.crew.common; - -import java.util.List; -import org.springframework.jdbc.core.JdbcTemplate; - -public class DatabaseCleaner { - - private final JdbcTemplate jdbcTemplate; - private final List truncateQueries; - - public DatabaseCleaner(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.truncateQueries = jdbcTemplate.queryForList(""" - SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';') AS query - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'PUBLIC' - """, String.class - ); - } - - public void clean() { - jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE"); - truncateQueries.forEach(jdbcTemplate::execute); - jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE"); - } -} 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/helper/CrewTestHelper.java b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java similarity index 92% rename from src/test/java/com/retrip/crew/common/helper/CrewTestHelper.java rename to src/test/java/com/retrip/crew/common/fixture/CrewFixture.java index d854d8a..2ed3b6a 100644 --- a/src/test/java/com/retrip/crew/common/helper/CrewTestHelper.java +++ b/src/test/java/com/retrip/crew/common/fixture/CrewFixture.java @@ -1,4 +1,4 @@ -package com.retrip.crew.common.helper; +package com.retrip.crew.common.fixture; import com.retrip.crew.application.in.request.CrewCreateRequest; import java.util.List; @@ -6,7 +6,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -public abstract class CrewTestHelper { +public abstract class CrewFixture { public static CrewCreateRequest createCrew(UUID memberId, String title, String description, int maxMembers) { return new CrewCreateRequest( 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 11c368e..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,14 +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); }