From 4f9b547b3e6aad4052712e8983db62665da88fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=B8=EC=84=B1?= Date: Sun, 5 Feb 2023 20:35:05 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EC=B5=9C=EA=B7=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EB=90=9C=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B0=9C=EB=B0=9C=20(#203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat] 지정한 개수 챌린지 조회 API 개발 * [Feat] 지정한 개수 챌린지 조회 API 개발 * [Fix] Fix typo * [Fix] 조인 방식 변경 --- .../component/ChallengeAssembler.java | 22 +++++++--- .../ccc/keeweapi/config/WebMvcConfig.java | 12 ++++++ .../controller/api/KeeweControllerAdvice.java | 8 ++++ .../api/challenge/ChallengeController.java | 28 +++++++++++-- .../dto/challenge/ChallengeInfoResponse.java | 27 +++++++++++++ .../challenge/ChallengeApiService.java | 10 ++++- .../challenge/ChallengeControllerTest.java | 40 +++++++++++++++++++ .../challenge/ChallengeQueryRepository.java | 24 +++++++++++ .../insight/InsightQueryRepository.java | 12 ++++++ .../challenge/ChallengeDomainService.java | 16 +++++--- .../service/insight/InsightDomainService.java | 5 +++ 11 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 keewe-api/src/main/java/ccc/keeweapi/config/WebMvcConfig.java create mode 100644 keewe-api/src/main/java/ccc/keeweapi/dto/challenge/ChallengeInfoResponse.java create mode 100644 keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/challenge/ChallengeQueryRepository.java diff --git a/keewe-api/src/main/java/ccc/keeweapi/component/ChallengeAssembler.java b/keewe-api/src/main/java/ccc/keeweapi/component/ChallengeAssembler.java index 94b2cba8..6091f70c 100644 --- a/keewe-api/src/main/java/ccc/keeweapi/component/ChallengeAssembler.java +++ b/keewe-api/src/main/java/ccc/keeweapi/component/ChallengeAssembler.java @@ -1,17 +1,25 @@ package ccc.keeweapi.component; -import ccc.keeweapi.dto.challenge.*; +import ccc.keeweapi.dto.challenge.ChallengeCreateRequest; +import ccc.keeweapi.dto.challenge.ChallengeCreateResponse; +import ccc.keeweapi.dto.challenge.ChallengeInfoResponse; +import ccc.keeweapi.dto.challenge.ChallengeParticipateRequest; +import ccc.keeweapi.dto.challenge.ChallengeParticipationResponse; +import ccc.keeweapi.dto.challenge.DayProgressResponse; +import ccc.keeweapi.dto.challenge.InsightProgressResponse; +import ccc.keeweapi.dto.challenge.ParticipatingChallengeResponse; +import ccc.keeweapi.dto.challenge.ParticipationCheckResponse; +import ccc.keeweapi.dto.challenge.WeekProgressResponse; import ccc.keeweapi.utils.SecurityUtil; -import ccc.keewedomain.persistence.domain.challenge.Challenge; -import ccc.keewedomain.persistence.domain.challenge.ChallengeParticipation; import ccc.keewedomain.dto.challenge.ChallengeCreateDto; import ccc.keewedomain.dto.challenge.ChallengeParticipateDto; -import org.springframework.stereotype.Component; - +import ccc.keewedomain.persistence.domain.challenge.Challenge; +import ccc.keewedomain.persistence.domain.challenge.ChallengeParticipation; import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.springframework.stereotype.Component; @Component public class ChallengeAssembler { @@ -104,4 +112,8 @@ public ParticipatingChallengeResponse toMyChallengeResponse(ChallengeParticipati participation.getCreatedAt().toLocalDate().toString() ); } + + public ChallengeInfoResponse toChallengeInfoResponse(Challenge challenge, Long insightCount) { + return ChallengeInfoResponse.of(challenge.getId(), challenge.getInterest(), challenge.getName(), challenge.getIntroduction(), insightCount); + } } diff --git a/keewe-api/src/main/java/ccc/keeweapi/config/WebMvcConfig.java b/keewe-api/src/main/java/ccc/keeweapi/config/WebMvcConfig.java new file mode 100644 index 00000000..08a0bdaa --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/config/WebMvcConfig.java @@ -0,0 +1,12 @@ +package ccc.keeweapi.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +public class WebMvcConfig implements WebMvcConfigurer { + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/controller/api/KeeweControllerAdvice.java b/keewe-api/src/main/java/ccc/keeweapi/controller/api/KeeweControllerAdvice.java index b1d782b3..418ad1da 100644 --- a/keewe-api/src/main/java/ccc/keeweapi/controller/api/KeeweControllerAdvice.java +++ b/keewe-api/src/main/java/ccc/keeweapi/controller/api/KeeweControllerAdvice.java @@ -4,6 +4,7 @@ import ccc.keewecore.consts.KeeweRtnConsts; import ccc.keewecore.exception.KeeweException; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -43,6 +44,13 @@ public ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotVal return ApiResponse.failure(KeeweRtnConsts.ERR400, messages); } + @ExceptionHandler(InvalidDataAccessApiUsageException.class) + @ResponseStatus(BAD_REQUEST) + public ApiResponse handleContraintViolationException(InvalidDataAccessApiUsageException ex) { + log.info("ConstraintViolationException: {}", ex.getMessage(), ex); + return ApiResponse.failure(KeeweRtnConsts.ERR400, ex.getCause().getMessage()); + } + @ExceptionHandler(Exception.class) public ApiResponse handleException(HttpServletRequest request, Exception ex) { ex.printStackTrace(); diff --git a/keewe-api/src/main/java/ccc/keeweapi/controller/api/challenge/ChallengeController.java b/keewe-api/src/main/java/ccc/keeweapi/controller/api/challenge/ChallengeController.java index 65bb16a7..83aee6ab 100644 --- a/keewe-api/src/main/java/ccc/keeweapi/controller/api/challenge/ChallengeController.java +++ b/keewe-api/src/main/java/ccc/keeweapi/controller/api/challenge/ChallengeController.java @@ -1,12 +1,27 @@ package ccc.keeweapi.controller.api.challenge; import ccc.keeweapi.dto.ApiResponse; -import ccc.keeweapi.dto.challenge.*; +import ccc.keeweapi.dto.challenge.ChallengeCreateRequest; +import ccc.keeweapi.dto.challenge.ChallengeCreateResponse; +import ccc.keeweapi.dto.challenge.ChallengeInfoResponse; +import ccc.keeweapi.dto.challenge.ChallengeParticipateRequest; +import ccc.keeweapi.dto.challenge.ChallengeParticipationResponse; +import ccc.keeweapi.dto.challenge.InsightProgressResponse; +import ccc.keeweapi.dto.challenge.ParticipatingChallengeResponse; +import ccc.keeweapi.dto.challenge.ParticipationCheckResponse; +import ccc.keeweapi.dto.challenge.WeekProgressResponse; import ccc.keeweapi.service.challenge.ChallengeApiService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - +import java.util.List; import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/challenge") @@ -43,4 +58,9 @@ public ApiResponse getMyThisWeekProgress() { public ApiResponse getParticipatingChallenge() { return ApiResponse.ok(challengeApiService.getParticipatingChallenege()); } + + @GetMapping("/specified-size") + public ApiResponse> getSpecifiedNumberOfChallenge(@RequestParam("size") @Min(1) @Max(10) Integer size) { + return ApiResponse.ok(challengeApiService.getSpecifiedNumberOfChallenge(size)); + } } diff --git a/keewe-api/src/main/java/ccc/keeweapi/dto/challenge/ChallengeInfoResponse.java b/keewe-api/src/main/java/ccc/keeweapi/dto/challenge/ChallengeInfoResponse.java new file mode 100644 index 00000000..e94a6d0e --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/dto/challenge/ChallengeInfoResponse.java @@ -0,0 +1,27 @@ +package ccc.keeweapi.dto.challenge; + +import static lombok.AccessLevel.PRIVATE; + +import ccc.keewedomain.persistence.domain.common.Interest; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +@Getter +public class ChallengeInfoResponse { + private Long challengeId; + private String challengeCategory; + private String challengeName; + private String challengeIntroduction; + private Long insightCount; + + public static ChallengeInfoResponse of(Long id, Interest interest, String name, String introduction, Long insightCount) { + ChallengeInfoResponse response = new ChallengeInfoResponse(); + response.challengeId = id; + response.challengeCategory = interest.getName(); + response.challengeName = name; + response.challengeIntroduction = introduction; + response.insightCount = insightCount; + return response; + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/challenge/ChallengeApiService.java b/keewe-api/src/main/java/ccc/keeweapi/service/challenge/ChallengeApiService.java index 15a2905e..da7709c7 100644 --- a/keewe-api/src/main/java/ccc/keeweapi/service/challenge/ChallengeApiService.java +++ b/keewe-api/src/main/java/ccc/keeweapi/service/challenge/ChallengeApiService.java @@ -10,11 +10,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Service @@ -80,6 +80,14 @@ public ParticipatingChallengeResponse getParticipatingChallenege() { .orElse(null); } + public List getSpecifiedNumberOfChallenge(int size) { + List specifiedNumberOfChallenge = challengeDomainService.getSpecifiedNumberOfRecentChallenge(size); + Map insightCountPerChallengeMap = insightDomainService.getInsightCountPerChallenge(specifiedNumberOfChallenge); + return specifiedNumberOfChallenge.stream() + .map(challenge -> challengeAssembler.toChallengeInfoResponse(challenge, insightCountPerChallengeMap.getOrDefault(challenge.getId(), 0L))) + .collect(Collectors.toList()); + } + private List datesOfWeek(LocalDate startDate) { List dates = new ArrayList<>(7); for (int i = 0; i < 7; i++) { diff --git a/keewe-api/src/test/java/ccc/keeweapi/controller/api/challenge/ChallengeControllerTest.java b/keewe-api/src/test/java/ccc/keeweapi/controller/api/challenge/ChallengeControllerTest.java index bda63f02..7b1f1036 100644 --- a/keewe-api/src/test/java/ccc/keeweapi/controller/api/challenge/ChallengeControllerTest.java +++ b/keewe-api/src/test/java/ccc/keeweapi/controller/api/challenge/ChallengeControllerTest.java @@ -3,6 +3,7 @@ import ccc.keeweapi.document.utils.ApiDocumentationTest; import ccc.keeweapi.dto.challenge.*; import ccc.keeweapi.service.challenge.ChallengeApiService; +import ccc.keewedomain.persistence.domain.common.Interest; import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -21,6 +22,7 @@ import static com.epages.restdocs.apispec.ResourceDocumentation.headerWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -295,4 +297,42 @@ void home_my_challenge() throws Exception { .build() ))); } + + @Test + @DisplayName("진행 중인 최근 챌린지 일부 조회") + void get_progress_recent_challenge() throws Exception { + List response = List.of( + ChallengeInfoResponse.of(3L, Interest.of("카테고리"), "챌린지명", "챌린지설명", 5L) + ); + + when(challengeApiService.getSpecifiedNumberOfChallenge(anyInt())) + .thenReturn(response); + + ResultActions resultActions = mockMvc.perform(get("/api/v1/challenge/specified-size") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + JWT) + .param("size", "5") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + resultActions.andDo(restDocs.document(resource( + ResourceSnippetParameters.builder() + .description("진행중인 최근 챌린지 현황 중 지정한 개수 만큼 조회하는 API 입니다.") + .summary("진행중인 최근 챌린지 현황 중 지정한 개수 만큼 조회하는 API") + .requestHeaders( + headerWithName("Authorization").description("유저의 JWT") + ) + .responseFields( + fieldWithPath("message").description("요청 결과 메세지"), + fieldWithPath("code").description("결과 코드"), + fieldWithPath("data").description("데이터, 오류 시 null"), + fieldWithPath("data[].challengeId").description("챌린지의 ID"), + fieldWithPath("data[].challengeName").description("챌린지의 이름"), + fieldWithPath("data[].challengeCategory").description("챌린지 카테고리"), + fieldWithPath("data[].challengeIntroduction").description("챌린지 설명"), + fieldWithPath("data[].insightCount").description("챌린지에 기록한 인사이트 수") + ) + .tag("Challenge") + .build() + ))); + } } diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/challenge/ChallengeQueryRepository.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/challenge/ChallengeQueryRepository.java new file mode 100644 index 00000000..695a9eb5 --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/challenge/ChallengeQueryRepository.java @@ -0,0 +1,24 @@ +package ccc.keewedomain.persistence.repository.challenge; + +import static ccc.keewedomain.persistence.domain.challenge.QChallenge.challenge; + +import ccc.keewedomain.persistence.domain.challenge.Challenge; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ChallengeQueryRepository { + private final JPAQueryFactory queryFactory; + + public List getSpecifiedNumberOfChallenge(int size) { + return queryFactory.select(challenge) + .from(challenge) + .where(challenge.deleted.isFalse()) + .orderBy(challenge.id.desc()) + .limit(size) + .fetch(); + } +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/insight/InsightQueryRepository.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/insight/InsightQueryRepository.java index 3b79c3f9..d20b6dfd 100644 --- a/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/insight/InsightQueryRepository.java +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/insight/InsightQueryRepository.java @@ -1,15 +1,18 @@ package ccc.keewedomain.persistence.repository.insight; +import ccc.keewedomain.persistence.domain.challenge.Challenge; import ccc.keewedomain.persistence.domain.challenge.ChallengeParticipation; import ccc.keewedomain.persistence.domain.insight.Insight; import ccc.keewedomain.persistence.domain.user.QUser; import ccc.keewedomain.persistence.domain.user.User; import ccc.keewedomain.persistence.repository.utils.CursorPageable; +import com.querydsl.core.group.GroupBy; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -121,6 +124,15 @@ public List findByUserIdAndDrawerId(Long userId, Long drawerId, CursorP .fetch(); } + public Map countPerChallenge(List challenges) { + return queryFactory + .from(insight) + .innerJoin(insight.challengeParticipation, challengeParticipation) + .innerJoin(challengeParticipation.challenge, challenge) + .where(challenge.in(challenges)) + .groupBy(challenge.id) + .transform(GroupBy.groupBy(challenge.id).as(insight.count())); + } private BooleanExpression drawerIdEq(Long drawerId) { return drawerId != null ? insight.drawer.id.eq(drawerId) : null; diff --git a/keewe-domain/src/main/java/ccc/keewedomain/service/challenge/ChallengeDomainService.java b/keewe-domain/src/main/java/ccc/keewedomain/service/challenge/ChallengeDomainService.java index 7d18de8e..71ba98ca 100644 --- a/keewe-domain/src/main/java/ccc/keewedomain/service/challenge/ChallengeDomainService.java +++ b/keewe-domain/src/main/java/ccc/keewedomain/service/challenge/ChallengeDomainService.java @@ -9,8 +9,10 @@ import ccc.keewedomain.persistence.domain.user.User; import ccc.keewedomain.persistence.repository.challenge.ChallengeParticipationQueryRepository; import ccc.keewedomain.persistence.repository.challenge.ChallengeParticipationRepository; +import ccc.keewedomain.persistence.repository.challenge.ChallengeQueryRepository; import ccc.keewedomain.persistence.repository.challenge.ChallengeRepository; import ccc.keewedomain.service.user.UserDomainService; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -26,9 +28,9 @@ @RequiredArgsConstructor public class ChallengeDomainService { private final ChallengeRepository challengeRepository; + private final ChallengeQueryRepository challengeQueryRepository; private final ChallengeParticipationRepository challengeParticipationRepository; private final ChallengeParticipationQueryRepository challengeParticipationQueryRepository; - private final UserDomainService userDomainService; public Challenge save(ChallengeCreateDto dto) { @@ -60,10 +62,6 @@ public Optional findCurrentParticipationWithChallenge(Lo return challengeParticipationQueryRepository.findByChallengerIdAndStatusWithChallenge(challengerId, CHALLENGING); } - private void exitCurrentChallengeIfExist(User challenger) { - findCurrentChallengeParticipation(challenger).ifPresent(ChallengeParticipation::cancel); - } - public Optional findCurrentChallengeParticipation(User challenger) { return challengeParticipationRepository.findByChallengerAndStatusAndDeletedFalse(challenger, CHALLENGING); } @@ -74,7 +72,15 @@ public Map getRecordCountPerDate(ChallengeParticipation participat return challengeParticipationQueryRepository.getRecordCountPerDate(participation, startDateTime, startDateTime.plusDays(7L)); } + public List getSpecifiedNumberOfRecentChallenge(int size) { + return challengeQueryRepository.getSpecifiedNumberOfChallenge(size); + } + public Long countParticipatingUser(Challenge challenge) { return challengeParticipationQueryRepository.countByChallengeAndStatus(challenge, CHALLENGING); } + + private void exitCurrentChallengeIfExist(User challenger) { + findCurrentChallengeParticipation(challenger).ifPresent(ChallengeParticipation::cancel); + } } diff --git a/keewe-domain/src/main/java/ccc/keewedomain/service/insight/InsightDomainService.java b/keewe-domain/src/main/java/ccc/keewedomain/service/insight/InsightDomainService.java index a32f55d5..64258485 100644 --- a/keewe-domain/src/main/java/ccc/keewedomain/service/insight/InsightDomainService.java +++ b/keewe-domain/src/main/java/ccc/keewedomain/service/insight/InsightDomainService.java @@ -9,6 +9,7 @@ import ccc.keewedomain.cache.repository.insight.CReactionCountRepository; import ccc.keewedomain.domain.insight.ReactionAggregation; import ccc.keewedomain.dto.insight.*; +import ccc.keewedomain.persistence.domain.challenge.Challenge; import ccc.keewedomain.persistence.domain.challenge.ChallengeParticipation; import ccc.keewedomain.persistence.domain.common.Link; import ccc.keewedomain.persistence.domain.insight.Bookmark; @@ -190,6 +191,10 @@ public List getInsightsForMyPage(User user, Long targetUserId, .collect(Collectors.toList()); } + public Map getInsightCountPerChallenge(List challenges) { + return insightQueryRepository.countPerChallenge(challenges); + } + /***************************************************************** ********************** private 메소드 영역 분리 ********************* *****************************************************************/