diff --git a/cider-api/build.gradle b/cider-api/build.gradle index c91c671..ff41e2f 100644 --- a/cider-api/build.gradle +++ b/cider-api/build.gradle @@ -52,6 +52,12 @@ dependencies { // s3 implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE' + + // querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'; + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } jar{ diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/controller/ChallengeController.java b/cider-api/src/main/java/com/cmc/domains/challenge/controller/ChallengeController.java index da2a859..5d6d712 100644 --- a/cider-api/src/main/java/com/cmc/domains/challenge/controller/ChallengeController.java +++ b/cider-api/src/main/java/com/cmc/domains/challenge/controller/ChallengeController.java @@ -1,30 +1,39 @@ package com.cmc.domains.challenge.controller; import com.cmc.challenge.Challenge; +import com.cmc.challengeLike.ChallengeLike; +import com.cmc.common.exception.BadRequestException; import com.cmc.common.response.CommonResponse; -import com.cmc.common.response.CreatedResponse; import com.cmc.domains.challenge.dto.request.ChallengeCreateRequestDto; import com.cmc.domains.challenge.dto.request.ChallengeParticipateRequestDto; import com.cmc.domains.challenge.dto.response.ChallengeCreateResponseDto; +import com.cmc.domains.challenge.dto.response.ChallengeHomeResponseDto; +import com.cmc.domains.challenge.dto.response.ChallengeResponseDto; import com.cmc.domains.challenge.service.ChallengeService; +import com.cmc.domains.challenge.vo.ChallengeResponseVo; import com.cmc.domains.image.service.ImageService; import com.cmc.domains.participate.service.ParticipateService; import com.cmc.global.resolver.RequestMemberId; -import com.cmc.participate.Participate; +import com.cmc.oauth.service.TokenProvider; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Slf4j @RestController @@ -65,6 +74,67 @@ public ResponseEntity createSuccessExampleImages(@Parameter(hidd return ResponseEntity.ok(CommonResponse.from("인증 예시 이미지가 업로드 되었습니다.")); } + @Tag(name = "challenge") + @Operation(summary = "홈 - 인기 챌린지, 공식 챌린지 조회 api") + @GetMapping("/home") + public ResponseEntity getChallengeHome(HttpServletRequest httpServletRequest) { + + final String tokenString = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); + + // 인기 챌린지 + List popularChallengeVos = challengeService.getPopularChallenges(); + List popularChallengeResponseDtos = makeChallengeResponseDto(tokenString, popularChallengeVos); + + // 공식 챌린지 + List officialChallengeVos = challengeService.getOfficialChallenges(); + List officialChallengeResponseDtos = makeChallengeResponseDto(tokenString, officialChallengeVos); + + return ResponseEntity.ok(ChallengeHomeResponseDto.from(popularChallengeResponseDtos, officialChallengeResponseDtos)); + } + + @Tag(name = "challenge") + @Operation(summary = "홈 - 카테고리 별 챌린지 조회 api") + @GetMapping("/home/{category}") + public ResponseEntity> getChallengeHomeCategory(HttpServletRequest httpServletRequest, + @PathVariable("category") String category) { + + final String tokenString = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); + + List challengeVos = challengeService.getCategoryChallenges(category); + List challengeResponseDtos = makeChallengeResponseDto(tokenString, challengeVos); + return ResponseEntity.ok(challengeResponseDtos); + } + + private List makeChallengeResponseDto(String tokenString, List challengeVos){ + + List challengeResponseDtos = new ArrayList<>(); + if (tokenString == null || tokenString.isEmpty()) { + // 로그인 x + challengeResponseDtos = challengeVos.stream().map(vo -> { + return ChallengeResponseDto.from(vo.getChallenge(), vo.getParticipateNum(), + ChronoUnit.DAYS.between(LocalDate.now(), vo.getChallenge().getChallengeStartDate())); + }).toList(); + } else{ + // 로그인 o + challengeResponseDtos = challengeVos.stream().map(vo -> { + return ChallengeResponseDto.from(vo.getChallenge(), vo.getParticipateNum(), + findIsLike(vo.getChallenge(), TokenProvider.getMemberId(tokenString)), ChronoUnit.DAYS.between(LocalDate.now(), vo.getChallenge().getChallengeStartDate())); + }).toList(); + } + + return challengeResponseDtos; + } + + private Boolean findIsLike(Challenge challenge, Long memberId){ + + for(ChallengeLike challengeLike : challenge.getChallengeLikes()){ + if (challengeLike.getMember().getMemberId().equals(memberId)){ + return true; + } + } + return false; + } + @Tag(name = "challenge") @Operation(summary = "챌린지 참여하기 api") @PostMapping(value="/participate") diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/dto/response/ChallengeHomeResponseDto.java b/cider-api/src/main/java/com/cmc/domains/challenge/dto/response/ChallengeHomeResponseDto.java new file mode 100644 index 0000000..2bf6c44 --- /dev/null +++ b/cider-api/src/main/java/com/cmc/domains/challenge/dto/response/ChallengeHomeResponseDto.java @@ -0,0 +1,30 @@ +package com.cmc.domains.challenge.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +public class ChallengeHomeResponseDto { + + @Schema(description = "인기 챌린지 리스트") + private List challengeResponseDto; + + @Schema(description = "공식 챌린지 리스트") + private List officialChallengeResponseDto; + + public static ChallengeHomeResponseDto from(List challengeResponseDto, + List officialChallengeResponseDto){ + + return new ChallengeHomeResponseDtoBuilder() + .challengeResponseDto(challengeResponseDto) + .officialChallengeResponseDto(officialChallengeResponseDto) + .build(); + } + +} diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/dto/response/ChallengeResponseDto.java b/cider-api/src/main/java/com/cmc/domains/challenge/dto/response/ChallengeResponseDto.java new file mode 100644 index 0000000..a9be217 --- /dev/null +++ b/cider-api/src/main/java/com/cmc/domains/challenge/dto/response/ChallengeResponseDto.java @@ -0,0 +1,75 @@ +package com.cmc.domains.challenge.dto.response; + +import com.cmc.challenge.Challenge; +import com.cmc.challenge.constant.Status; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +public class ChallengeResponseDto { + + @Schema(description = "챌린지 id", example = "10") + private Long challengeId; + + @Schema(description = "챌린지 제목", example = "소비습관 고치기") + private String challengeName; + + @Schema(description = "챌린지 상태", example = "RECRUITING: 모집중, POSSIBLE: 참여 가능, IMPOSSIBLE: 참여 불가(종료)") + private Status challengeStatus; + + @Schema(description = "챌린지 대기/참여중 인원", example = "5") + private Integer participateNum; + + @Schema(description = "모집중인 경우 - 디데이", example = "23") + private Long recruitLeft; + + @Schema(description = "챌린지 분야", example = "재태크/돈관리/금융학습/소비절약") + private String interestField; + + @Schema(description = "챌린지 진행 기간", example = "4") + private Integer challengePeriod; + + @Schema(description = "공식 챌린지 여부", example = "true") + private Boolean isOfficial; + + @Schema(description = "리워드 여부", example = "true") + private Boolean isReward; + + @Schema(description = "로그인 한 사용자 - 챌린지 좋아요 여부", example = "false") + private Boolean isLike; + + public static ChallengeResponseDto from(Challenge challenge, Integer participateNum, Boolean isLike, Long recruitLeft){ + + return new ChallengeResponseDtoBuilder() + .challengeId(challenge.getChallengeId()) + .challengeName(challenge.getChallengeName()) + .challengeStatus(challenge.getChallengeStatus()) + .participateNum(participateNum) + .recruitLeft(recruitLeft) + .interestField(challenge.getChallengeBranch()) + .isOfficial(challenge.getIsOfficial()) + .isReward(challenge.getIsReward()) + .isLike(isLike) + .build(); + } + + public static ChallengeResponseDto from(Challenge challenge, Integer participateNum, Long recruitLeft){ + + return new ChallengeResponseDtoBuilder() + .challengeId(challenge.getChallengeId()) + .challengeName(challenge.getChallengeName()) + .challengeStatus(challenge.getChallengeStatus()) + .participateNum(participateNum) + .recruitLeft(recruitLeft) + .interestField(challenge.getChallengeBranch()) + .isOfficial(challenge.getIsOfficial()) + .isReward(challenge.getIsReward()) + .isLike(false) + .build(); + } + +} diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeCustomRepository.java b/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeCustomRepository.java new file mode 100644 index 0000000..9a0b786 --- /dev/null +++ b/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeCustomRepository.java @@ -0,0 +1,14 @@ +package com.cmc.domains.challenge.repository; + +import com.cmc.domains.challenge.vo.ChallengeResponseVo; + +import java.util.List; + +public interface ChallengeCustomRepository { + + List getPopularChallenges(); + + List getOfficialChallenges(); + + List getCategoryChallenges(String category); +} diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeCustomRepositoryImpl.java b/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeCustomRepositoryImpl.java new file mode 100644 index 0000000..0130229 --- /dev/null +++ b/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeCustomRepositoryImpl.java @@ -0,0 +1,78 @@ +package com.cmc.domains.challenge.repository; + +import com.cmc.challenge.QChallenge; +import com.cmc.challenge.constant.InterestField; +import com.cmc.challenge.constant.Status; +import com.cmc.domains.challenge.vo.ChallengeResponseVo; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.List; + +import static com.cmc.challenge.QChallenge.challenge; +import static com.cmc.participate.QParticipate.participate; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class ChallengeCustomRepositoryImpl implements ChallengeCustomRepository{ + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List getPopularChallenges() { + + return jpaQueryFactory.selectDistinct(Projections.fields(ChallengeResponseVo.class, + challenge, + participate.count())) + .from(challenge, participate) + .innerJoin(participate.challenge, challenge) + .where(challenge.challengeStatus.eq(Status.RECRUITING).or(challenge.challengeStatus.eq(Status.POSSIBLE))) + .groupBy(challenge) + .orderBy(participate.count().desc()) + .limit(10) + .fetch(); + } + + @Override + public List getOfficialChallenges() { + + return jpaQueryFactory.selectDistinct(Projections.fields(ChallengeResponseVo.class, + challenge, + participate.count())) + .from(challenge, participate) + .innerJoin(participate.challenge, challenge) + .where(challenge.challengeStatus.eq(Status.POSSIBLE).and(challenge.isOfficial.eq(true))) + .groupBy(challenge) + .orderBy(challenge.createdDate.desc()) + .limit(10) + .fetch(); + } + + @Override + public List getCategoryChallenges(String category) { + + return jpaQueryFactory.selectDistinct(Projections.fields(ChallengeResponseVo.class, + challenge, + participate.count())) + .from(challenge, participate) + .innerJoin(participate.challenge, challenge) + .where(challenge.challengeStatus.eq(Status.RECRUITING).or(challenge.challengeStatus.eq(Status.POSSIBLE)) + .and(challenge.challengeBranch.eq(category))) + .groupBy(challenge) + .fetch(); + } + + private Long getDateBetween(QChallenge challenge){ + + return ChronoUnit.DAYS.between((Temporal) challenge.challengeStartDate, LocalDate.now()); + } + +} diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeRepository.java b/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeRepository.java index 7e2ac29..c02a973 100644 --- a/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeRepository.java +++ b/cider-api/src/main/java/com/cmc/domains/challenge/repository/ChallengeRepository.java @@ -1,11 +1,15 @@ package com.cmc.domains.challenge.repository; import com.cmc.challenge.Challenge; +import com.cmc.domains.challenge.vo.ChallengeResponseVo; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -@Repository -public interface ChallengeRepository extends JpaRepository { +import java.util.List; +@Repository +public interface ChallengeRepository extends JpaRepository, ChallengeCustomRepository { } diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/service/ChallengeService.java b/cider-api/src/main/java/com/cmc/domains/challenge/service/ChallengeService.java index 33b1912..f809d3a 100644 --- a/cider-api/src/main/java/com/cmc/domains/challenge/service/ChallengeService.java +++ b/cider-api/src/main/java/com/cmc/domains/challenge/service/ChallengeService.java @@ -3,10 +3,13 @@ import com.cmc.challenge.Challenge; import com.cmc.domains.challenge.dto.request.ChallengeCreateRequestDto; import com.cmc.domains.challenge.repository.ChallengeRepository; +import com.cmc.domains.challenge.vo.ChallengeResponseVo; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @Transactional @RequiredArgsConstructor @@ -20,4 +23,22 @@ public Challenge create(ChallengeCreateRequestDto req, Long memberId) { Challenge challenge = req.toEntity(); return challengeRepository.save(challenge); } + + // 인기 챌린지 조회 + public List getPopularChallenges() { + + return challengeRepository.getPopularChallenges(); + } + + // 공식 챌린지 조회 + public List getOfficialChallenges() { + + return challengeRepository.getOfficialChallenges(); + } + + // 카테고리 별 챌린지 조회 + public List getCategoryChallenges(String category) { + + return challengeRepository.getCategoryChallenges(category); + } } diff --git a/cider-api/src/main/java/com/cmc/domains/challenge/vo/ChallengeResponseVo.java b/cider-api/src/main/java/com/cmc/domains/challenge/vo/ChallengeResponseVo.java new file mode 100644 index 0000000..73b6912 --- /dev/null +++ b/cider-api/src/main/java/com/cmc/domains/challenge/vo/ChallengeResponseVo.java @@ -0,0 +1,19 @@ +package com.cmc.domains.challenge.vo; + +import com.cmc.challenge.Challenge; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChallengeResponseVo { + + private Challenge challenge; + + private Integer participateNum; + +} diff --git a/cider-api/src/main/java/com/cmc/global/config/QuerydslConfig.java b/cider-api/src/main/java/com/cmc/global/config/QuerydslConfig.java new file mode 100644 index 0000000..d0b9b46 --- /dev/null +++ b/cider-api/src/main/java/com/cmc/global/config/QuerydslConfig.java @@ -0,0 +1,18 @@ +package com.cmc.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/cider-domain/build.gradle b/cider-domain/build.gradle index 6330e66..f9fd441 100644 --- a/cider-domain/build.gradle +++ b/cider-domain/build.gradle @@ -16,6 +16,22 @@ dependencies { // jakarta implementation 'org.hibernate:hibernate-spatial:6.2.2.Final' + + // data jpa + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // mysql + implementation 'mysql:mysql-connector-java:8.0.33' + + // h2 + runtimeOnly 'com.h2database:h2' + + // querydsl + // TODO : 멀티모듈 의존성 정리 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'; + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } test { diff --git a/cider-domain/src/main/java/com/cmc/challenge/Challenge.java b/cider-domain/src/main/java/com/cmc/challenge/Challenge.java index 31b5a84..fcc9f62 100644 --- a/cider-domain/src/main/java/com/cmc/challenge/Challenge.java +++ b/cider-domain/src/main/java/com/cmc/challenge/Challenge.java @@ -1,6 +1,7 @@ package com.cmc.challenge; import com.cmc.base.BaseTimeEntity; +import com.cmc.challenge.constant.Status; import com.cmc.challengeLike.ChallengeLike; import com.cmc.image.certifyExample.CertifyExampleImage; import com.cmc.member.Member; @@ -11,6 +12,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -47,14 +49,24 @@ public class Challenge extends BaseTimeEntity { private Integer challengePeriod; + private LocalDate challengeStartDate; + private Integer recruitPeriod; private String certifyMission; private Boolean isPublic; + private Boolean isOfficial; + + private Boolean isReward; + private Integer certifyNum; + @Enumerated(EnumType.STRING) + @Column(name = "challenge_status", columnDefinition = "VARCHAR(30)") + private Status challengeStatus; + @Builder.Default @OneToMany(mappedBy = "challenge", fetch = FetchType.LAZY) private List certifyExampleImageList = new ArrayList<>(); diff --git a/cider-domain/src/main/java/com/cmc/challenge/constant/Status.java b/cider-domain/src/main/java/com/cmc/challenge/constant/Status.java new file mode 100644 index 0000000..19e5a48 --- /dev/null +++ b/cider-domain/src/main/java/com/cmc/challenge/constant/Status.java @@ -0,0 +1,24 @@ +package com.cmc.challenge.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Optional; + +@Getter +@AllArgsConstructor +public enum Status { + + RECRUITING("R", "모집중"), + POSSIBLE("P", "참여 가능"), + IMPOSSIBLE("I", "참여 불가능") + ; + + private String alias; + private String description; + + public static Optional of(String alias) { + return Arrays.stream(values()).filter(S -> S.alias.equals(alias)).findFirst(); + } +}