Skip to content

Commit

Permalink
[Feat] 최근 생성된 챌린지 조회 API 개발 (#203)
Browse files Browse the repository at this point in the history
* [Feat] 지정한 개수 챌린지 조회 API 개발

* [Feat] 지정한 개수 챌린지 조회 API 개발

* [Fix] Fix typo

* [Fix] 조인 방식 변경
  • Loading branch information
Youhoseong authored Feb 5, 2023
1 parent ca18fdf commit 4f9b547
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
}
}
12 changes: 12 additions & 0 deletions keewe-api/src/main/java/ccc/keeweapi/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -43,4 +58,9 @@ public ApiResponse<WeekProgressResponse> getMyThisWeekProgress() {
public ApiResponse<ParticipatingChallengeResponse> getParticipatingChallenge() {
return ApiResponse.ok(challengeApiService.getParticipatingChallenege());
}

@GetMapping("/specified-size")
public ApiResponse<List<ChallengeInfoResponse>> getSpecifiedNumberOfChallenge(@RequestParam("size") @Min(1) @Max(10) Integer size) {
return ApiResponse.ok(challengeApiService.getSpecifiedNumberOfChallenge(size));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,6 +80,14 @@ public ParticipatingChallengeResponse getParticipatingChallenege() {
.orElse(null);
}

public List<ChallengeInfoResponse> getSpecifiedNumberOfChallenge(int size) {
List<Challenge> specifiedNumberOfChallenge = challengeDomainService.getSpecifiedNumberOfRecentChallenge(size);
Map<Long, Long> insightCountPerChallengeMap = insightDomainService.getInsightCountPerChallenge(specifiedNumberOfChallenge);
return specifiedNumberOfChallenge.stream()
.map(challenge -> challengeAssembler.toChallengeInfoResponse(challenge, insightCountPerChallengeMap.getOrDefault(challenge.getId(), 0L)))
.collect(Collectors.toList());
}

private List<String> datesOfWeek(LocalDate startDate) {
List<String> dates = new ArrayList<>(7);
for (int i = 0; i < 7; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -295,4 +297,42 @@ void home_my_challenge() throws Exception {
.build()
)));
}

@Test
@DisplayName("진행 중인 최근 챌린지 일부 조회")
void get_progress_recent_challenge() throws Exception {
List<ChallengeInfoResponse> 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()
)));
}
}
Original file line number Diff line number Diff line change
@@ -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<Challenge> getSpecifiedNumberOfChallenge(int size) {
return queryFactory.select(challenge)
.from(challenge)
.where(challenge.deleted.isFalse())
.orderBy(challenge.id.desc())
.limit(size)
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -121,6 +124,15 @@ public List<Insight> findByUserIdAndDrawerId(Long userId, Long drawerId, CursorP
.fetch();
}

public Map<Long, Long> countPerChallenge(List<Challenge> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -60,10 +62,6 @@ public Optional<ChallengeParticipation> findCurrentParticipationWithChallenge(Lo
return challengeParticipationQueryRepository.findByChallengerIdAndStatusWithChallenge(challengerId, CHALLENGING);
}

private void exitCurrentChallengeIfExist(User challenger) {
findCurrentChallengeParticipation(challenger).ifPresent(ChallengeParticipation::cancel);
}

public Optional<ChallengeParticipation> findCurrentChallengeParticipation(User challenger) {
return challengeParticipationRepository.findByChallengerAndStatusAndDeletedFalse(challenger, CHALLENGING);
}
Expand All @@ -74,7 +72,15 @@ public Map<String, Long> getRecordCountPerDate(ChallengeParticipation participat
return challengeParticipationQueryRepository.getRecordCountPerDate(participation, startDateTime, startDateTime.plusDays(7L));
}

public List<Challenge> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,6 +191,10 @@ public List<InsightMyPageDto> getInsightsForMyPage(User user, Long targetUserId,
.collect(Collectors.toList());
}

public Map<Long, Long> getInsightCountPerChallenge(List<Challenge> challenges) {
return insightQueryRepository.countPerChallenge(challenges);
}

/*****************************************************************
********************** private 메소드 영역 분리 *********************
*****************************************************************/
Expand Down

0 comments on commit 4f9b547

Please sign in to comment.